@vue-skuilder/db 0.1.23 → 0.1.25

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 (80) hide show
  1. package/dist/{contentSource-BP9hznNV.d.ts → contentSource-BmnmvH8C.d.ts} +268 -3
  2. package/dist/{contentSource-DsJadoBU.d.cts → contentSource-DfBbaLA-.d.cts} +268 -3
  3. package/dist/core/index.d.cts +310 -6
  4. package/dist/core/index.d.ts +310 -6
  5. package/dist/core/index.js +2606 -666
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +2564 -639
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-CHYrQ5pB.d.cts → dataLayerProvider-BeRXVMs5.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-MDTxXq2l.d.ts → dataLayerProvider-CG9GfaAY.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 +2336 -656
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +2316 -631
  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 +2312 -632
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +2315 -630
  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 +278 -20
  26. package/dist/index.d.ts +278 -20
  27. package/dist/index.js +3603 -720
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +3529 -674
  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 +210 -9
  40. package/docs/todo-review-urgency-adaptation.md +205 -0
  41. package/docs/todo-strategy-authoring.md +8 -6
  42. package/package.json +3 -3
  43. package/src/core/index.ts +2 -0
  44. package/src/core/interfaces/contentSource.ts +7 -0
  45. package/src/core/interfaces/userDB.ts +50 -0
  46. package/src/core/navigators/Pipeline.ts +132 -5
  47. package/src/core/navigators/PipelineAssembler.ts +21 -22
  48. package/src/core/navigators/PipelineDebugger.ts +426 -0
  49. package/src/core/navigators/filters/WeightedFilter.ts +141 -0
  50. package/src/core/navigators/filters/types.ts +4 -0
  51. package/src/core/navigators/generators/CompositeGenerator.ts +82 -19
  52. package/src/core/navigators/generators/elo.ts +14 -1
  53. package/src/core/navigators/generators/srs.ts +146 -18
  54. package/src/core/navigators/generators/types.ts +4 -0
  55. package/src/core/navigators/index.ts +203 -13
  56. package/src/core/orchestration/gradient.ts +133 -0
  57. package/src/core/orchestration/index.ts +210 -0
  58. package/src/core/orchestration/learning.ts +250 -0
  59. package/src/core/orchestration/recording.ts +92 -0
  60. package/src/core/orchestration/signal.ts +67 -0
  61. package/src/core/types/contentNavigationStrategy.ts +38 -0
  62. package/src/core/types/learningState.ts +77 -0
  63. package/src/core/types/types-legacy.ts +4 -0
  64. package/src/core/types/userOutcome.ts +51 -0
  65. package/src/courseConfigRegistration.ts +107 -0
  66. package/src/factory.ts +6 -0
  67. package/src/impl/common/BaseUserDB.ts +16 -0
  68. package/src/impl/couch/user-course-relDB.ts +12 -0
  69. package/src/study/MixerDebugger.ts +555 -0
  70. package/src/study/SessionController.ts +159 -20
  71. package/src/study/SessionDebugger.ts +442 -0
  72. package/src/study/SourceMixer.ts +36 -17
  73. package/src/study/TODO-session-scheduling.md +133 -0
  74. package/src/study/index.ts +2 -0
  75. package/src/study/services/EloService.ts +79 -4
  76. package/src/study/services/ResponseProcessor.ts +130 -72
  77. package/src/study/services/SrsService.ts +9 -0
  78. package/tests/core/navigators/Pipeline.test.ts +2 -0
  79. package/tests/core/navigators/PipelineAssembler.test.ts +4 -4
  80. 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,359 +627,317 @@ 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 */;
630
+ // src/core/navigators/PipelineDebugger.ts
631
+ var PipelineDebugger_exports = {};
632
+ __export(PipelineDebugger_exports, {
633
+ buildRunReport: () => buildRunReport,
634
+ captureRun: () => captureRun,
635
+ mountPipelineDebugger: () => mountPipelineDebugger,
636
+ pipelineDebugAPI: () => pipelineDebugAPI
637
+ });
638
+ function getOrigin(card) {
639
+ const firstEntry = card.provenance[0];
640
+ if (!firstEntry) return "unknown";
641
+ const reason = firstEntry.reason?.toLowerCase() || "";
642
+ if (reason.includes("new card")) return "new";
643
+ if (reason.includes("review")) return "review";
644
+ return "unknown";
626
645
  }
627
- function isFilter(impl) {
628
- return NavigatorRoles[impl] === "filter" /* FILTER */;
646
+ function captureRun(report) {
647
+ const fullReport = {
648
+ ...report,
649
+ runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
650
+ timestamp: /* @__PURE__ */ new Date()
651
+ };
652
+ runHistory.unshift(fullReport);
653
+ if (runHistory.length > MAX_RUNS) {
654
+ runHistory.pop();
655
+ }
629
656
  }
630
- var NavigatorRoles, ContentNavigator;
631
- var init_navigators = __esm({
632
- "src/core/navigators/index.ts"() {
657
+ function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards) {
658
+ const selectedIds = new Set(selectedCards.map((c) => c.cardId));
659
+ const cards = allCards.map((card) => ({
660
+ cardId: card.cardId,
661
+ courseId: card.courseId,
662
+ origin: getOrigin(card),
663
+ finalScore: card.score,
664
+ provenance: card.provenance,
665
+ selected: selectedIds.has(card.cardId)
666
+ }));
667
+ const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
668
+ const newSelected = selectedCards.filter((c) => getOrigin(c) === "new").length;
669
+ return {
670
+ courseId,
671
+ courseName,
672
+ generatorName,
673
+ generators,
674
+ generatedCount,
675
+ filters,
676
+ finalCount: selectedCards.length,
677
+ reviewsSelected,
678
+ newSelected,
679
+ cards
680
+ };
681
+ }
682
+ function formatProvenance(provenance) {
683
+ return provenance.map((p) => {
684
+ const actionSymbol = p.action === "generated" ? "\u{1F3B2}" : p.action === "boosted" ? "\u2B06\uFE0F" : p.action === "penalized" ? "\u2B07\uFE0F" : "\u27A1\uFE0F";
685
+ return ` ${actionSymbol} ${p.strategyName}: ${p.score.toFixed(3)} - ${p.reason}`;
686
+ }).join("\n");
687
+ }
688
+ function printRunSummary(run) {
689
+ console.group(`\u{1F50D} Pipeline Run: ${run.courseId} (${run.courseName || "unnamed"})`);
690
+ logger.info(`Run ID: ${run.runId}`);
691
+ logger.info(`Time: ${run.timestamp.toISOString()}`);
692
+ logger.info(`Generator: ${run.generatorName} \u2192 ${run.generatedCount} candidates`);
693
+ if (run.generators && run.generators.length > 0) {
694
+ console.group("Generator breakdown:");
695
+ for (const g of run.generators) {
696
+ logger.info(
697
+ ` ${g.name}: ${g.cardCount} cards (${g.newCount} new, ${g.reviewCount} reviews, top: ${g.topScore.toFixed(2)})`
698
+ );
699
+ }
700
+ console.groupEnd();
701
+ }
702
+ if (run.filters.length > 0) {
703
+ console.group("Filter impact:");
704
+ for (const f of run.filters) {
705
+ logger.info(` ${f.name}: \u2191${f.boosted} \u2193${f.penalized} =${f.passed} \u2715${f.removed}`);
706
+ }
707
+ console.groupEnd();
708
+ }
709
+ logger.info(
710
+ `Result: ${run.finalCount} cards selected (${run.newSelected} new, ${run.reviewsSelected} reviews)`
711
+ );
712
+ console.groupEnd();
713
+ }
714
+ function mountPipelineDebugger() {
715
+ if (typeof window === "undefined") return;
716
+ const win = window;
717
+ win.skuilder = win.skuilder || {};
718
+ win.skuilder.pipeline = pipelineDebugAPI;
719
+ }
720
+ var MAX_RUNS, runHistory, pipelineDebugAPI;
721
+ var init_PipelineDebugger = __esm({
722
+ "src/core/navigators/PipelineDebugger.ts"() {
633
723
  "use strict";
634
724
  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;
725
+ MAX_RUNS = 10;
726
+ runHistory = [];
727
+ pipelineDebugAPI = {
652
728
  /**
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.
729
+ * Get raw run history for programmatic access.
658
730
  */
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;
665
- }
666
- }
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
- // ============================================================================
731
+ get runs() {
732
+ return [...runHistory];
733
+ },
675
734
  /**
676
- * Unique key identifying this strategy for state storage.
677
- *
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.
735
+ * Show summary of a specific pipeline run.
681
736
  */
682
- get strategyKey() {
683
- return this.constructor.name;
684
- }
737
+ showRun(idOrIndex = 0) {
738
+ if (runHistory.length === 0) {
739
+ logger.info("[Pipeline Debug] No runs captured yet.");
740
+ return;
741
+ }
742
+ let run;
743
+ if (typeof idOrIndex === "number") {
744
+ run = runHistory[idOrIndex];
745
+ if (!run) {
746
+ logger.info(
747
+ `[Pipeline Debug] No run found at index ${idOrIndex}. History length: ${runHistory.length}`
748
+ );
749
+ return;
750
+ }
751
+ } else {
752
+ run = runHistory.find((r) => r.runId.endsWith(idOrIndex));
753
+ if (!run) {
754
+ logger.info(`[Pipeline Debug] No run found matching ID '${idOrIndex}'.`);
755
+ return;
756
+ }
757
+ }
758
+ printRunSummary(run);
759
+ },
685
760
  /**
686
- * Get this strategy's persisted state for the current course.
687
- *
688
- * @returns The strategy's data payload, or null if no state exists
689
- * @throws Error if user or course is not initialized
761
+ * Show summary of the last pipeline run.
690
762
  */
691
- async getStrategyState() {
692
- if (!this.user || !this.course) {
693
- throw new Error(
694
- `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
695
- );
696
- }
697
- return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
698
- }
763
+ showLastRun() {
764
+ this.showRun(0);
765
+ },
699
766
  /**
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
767
+ * Show detailed provenance for a specific card.
704
768
  */
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
- );
769
+ showCard(cardId) {
770
+ for (const run of runHistory) {
771
+ const card = run.cards.find((c) => c.cardId === cardId);
772
+ if (card) {
773
+ console.group(`\u{1F3B4} Card: ${cardId}`);
774
+ logger.info(`Course: ${card.courseId}`);
775
+ logger.info(`Origin: ${card.origin}`);
776
+ logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
777
+ logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
778
+ logger.info("Provenance:");
779
+ logger.info(formatProvenance(card.provenance));
780
+ console.groupEnd();
781
+ return;
782
+ }
710
783
  }
711
- return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
712
- }
784
+ logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
785
+ },
713
786
  /**
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.
787
+ * Explain why reviews may or may not have been selected.
720
788
  */
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);
789
+ explainReviews() {
790
+ if (runHistory.length === 0) {
791
+ logger.info("[Pipeline Debug] No runs captured yet.");
792
+ return;
793
+ }
794
+ console.group("\u{1F4CB} Review Selection Analysis");
795
+ for (const run of runHistory) {
796
+ console.group(`Run: ${run.courseId} @ ${run.timestamp.toLocaleTimeString()}`);
797
+ const allReviews = run.cards.filter((c) => c.origin === "review");
798
+ const selectedReviews = allReviews.filter((c) => c.selected);
799
+ if (allReviews.length === 0) {
800
+ logger.info("\u274C No reviews were generated. Check SRS logs for why.");
801
+ } else if (selectedReviews.length === 0) {
802
+ logger.info(`\u26A0\uFE0F ${allReviews.length} reviews generated but none selected.`);
803
+ logger.info("Possible reasons:");
804
+ const topNewScore = Math.max(
805
+ ...run.cards.filter((c) => c.origin === "new" && c.selected).map((c) => c.finalScore),
806
+ 0
807
+ );
808
+ const topReviewScore = Math.max(...allReviews.map((c) => c.finalScore), 0);
809
+ if (topReviewScore < topNewScore) {
810
+ logger.info(
811
+ ` - New cards scored higher (top new: ${topNewScore.toFixed(2)}, top review: ${topReviewScore.toFixed(2)})`
812
+ );
735
813
  }
814
+ const topReview = allReviews.sort((a, b) => b.finalScore - a.finalScore)[0];
815
+ if (topReview) {
816
+ logger.info(` - Top review score: ${topReview.finalScore.toFixed(3)}`);
817
+ logger.info(" - Its provenance:");
818
+ logger.info(formatProvenance(topReview.provenance));
819
+ }
820
+ } else {
821
+ logger.info(`\u2705 ${selectedReviews.length}/${allReviews.length} reviews selected.`);
822
+ logger.info("Top selected review:");
823
+ const topSelected = selectedReviews.sort((a, b) => b.finalScore - a.finalScore)[0];
824
+ logger.info(formatProvenance(topSelected.provenance));
736
825
  }
826
+ console.groupEnd();
737
827
  }
738
- if (!NavigatorImpl) {
739
- throw new Error(`Could not load navigator implementation for: ${implementingClass}`);
828
+ console.groupEnd();
829
+ },
830
+ /**
831
+ * Show all runs in compact format.
832
+ */
833
+ listRuns() {
834
+ if (runHistory.length === 0) {
835
+ logger.info("[Pipeline Debug] No runs captured yet.");
836
+ return;
740
837
  }
741
- return new NavigatorImpl(user, course, strategyData);
742
- }
838
+ console.table(
839
+ runHistory.map((r) => ({
840
+ id: r.runId.slice(-8),
841
+ time: r.timestamp.toLocaleTimeString(),
842
+ course: r.courseName || r.courseId.slice(0, 8),
843
+ generated: r.generatedCount,
844
+ selected: r.finalCount,
845
+ new: r.newSelected,
846
+ reviews: r.reviewsSelected
847
+ }))
848
+ );
849
+ },
743
850
  /**
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.
751
- *
752
- * ## Implementation Required
753
- * All navigation strategies MUST override this method. The base class does
754
- * not provide a default implementation.
755
- *
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'.
760
- *
761
- * ## For Filters
762
- * Filters should implement the CardFilter interface instead and be composed
763
- * via Pipeline. Filters do not directly implement getWeightedCards().
764
- *
765
- * @param limit - Maximum cards to return
766
- * @returns Cards sorted by score descending, with provenance trails
851
+ * Export run history as JSON for bug reports.
767
852
  */
768
- async getWeightedCards(_limit) {
769
- throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
853
+ export() {
854
+ const json = JSON.stringify(runHistory, null, 2);
855
+ logger.info("[Pipeline Debug] Run history exported. Copy the returned string or use:");
856
+ logger.info(" copy(window.skuilder.pipeline.export())");
857
+ return json;
858
+ },
859
+ /**
860
+ * Clear run history.
861
+ */
862
+ clear() {
863
+ runHistory.length = 0;
864
+ logger.info("[Pipeline Debug] Run history cleared.");
865
+ },
866
+ /**
867
+ * Show help.
868
+ */
869
+ help() {
870
+ logger.info(`
871
+ \u{1F527} Pipeline Debug API
872
+
873
+ Commands:
874
+ .showLastRun() Show summary of most recent pipeline run
875
+ .showRun(id|index) Show summary of a specific run (by index or ID suffix)
876
+ .showCard(cardId) Show provenance trail for a specific card
877
+ .explainReviews() Analyze why reviews were/weren't selected
878
+ .listRuns() List all captured runs in table format
879
+ .export() Export run history as JSON for bug reports
880
+ .clear() Clear run history
881
+ .runs Access raw run history array
882
+ .help() Show this help message
883
+
884
+ Example:
885
+ window.skuilder.pipeline.showLastRun()
886
+ window.skuilder.pipeline.showRun(1)
887
+ window.skuilder.pipeline.showCard('abc123')
888
+ `);
770
889
  }
771
890
  };
891
+ mountPipelineDebugger();
772
892
  }
773
893
  });
774
894
 
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) {
792
- const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
793
- logger.info(
794
- `[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
795
- );
796
- }
797
- function logCardProvenance(cards, maxCards = 3) {
798
- const cardsToLog = cards.slice(0, maxCards);
799
- logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
800
- for (const card of cardsToLog) {
801
- logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
802
- for (const entry of card.provenance) {
803
- const scoreChange = entry.score.toFixed(3);
804
- const action = entry.action.padEnd(9);
805
- logger.debug(
806
- `[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
807
- );
808
- }
809
- }
810
- }
811
- var import_common5, Pipeline;
812
- var init_Pipeline = __esm({
813
- "src/core/navigators/Pipeline.ts"() {
895
+ // src/core/navigators/generators/CompositeGenerator.ts
896
+ var CompositeGenerator_exports = {};
897
+ __export(CompositeGenerator_exports, {
898
+ AggregationMode: () => AggregationMode,
899
+ default: () => CompositeGenerator
900
+ });
901
+ var AggregationMode, DEFAULT_AGGREGATION_MODE, FREQUENCY_BOOST_FACTOR, CompositeGenerator;
902
+ var init_CompositeGenerator = __esm({
903
+ "src/core/navigators/generators/CompositeGenerator.ts"() {
814
904
  "use strict";
815
- import_common5 = require("@vue-skuilder/common");
816
905
  init_navigators();
817
906
  init_logger();
818
- Pipeline = class extends ContentNavigator {
819
- generator;
820
- filters;
907
+ AggregationMode = /* @__PURE__ */ ((AggregationMode2) => {
908
+ AggregationMode2["MAX"] = "max";
909
+ AggregationMode2["AVERAGE"] = "average";
910
+ AggregationMode2["FREQUENCY_BOOST"] = "frequencyBoost";
911
+ return AggregationMode2;
912
+ })(AggregationMode || {});
913
+ DEFAULT_AGGREGATION_MODE = "frequencyBoost" /* FREQUENCY_BOOST */;
914
+ FREQUENCY_BOOST_FACTOR = 0.1;
915
+ CompositeGenerator = class _CompositeGenerator extends ContentNavigator {
916
+ /** Human-readable name for CardGenerator interface */
917
+ name = "Composite Generator";
918
+ generators;
919
+ aggregationMode;
920
+ constructor(generators, aggregationMode = DEFAULT_AGGREGATION_MODE) {
921
+ super();
922
+ this.generators = generators;
923
+ this.aggregationMode = aggregationMode;
924
+ if (generators.length === 0) {
925
+ throw new Error("CompositeGenerator requires at least one generator");
926
+ }
927
+ logger.debug(
928
+ `[CompositeGenerator] Created with ${generators.length} generators, mode: ${aggregationMode}`
929
+ );
930
+ }
821
931
  /**
822
- * Create a new pipeline.
932
+ * Creates a CompositeGenerator from strategy data.
823
933
  *
824
- * @param generator - The generator (or CompositeGenerator) that produces candidates
825
- * @param filters - Filters to apply sequentially (order doesn't matter for multipliers)
826
- * @param user - User database interface
827
- * @param course - Course database interface
934
+ * This is a convenience factory for use by PipelineAssembler.
828
935
  */
829
- constructor(generator, filters, user, course) {
830
- super();
831
- this.generator = generator;
832
- this.filters = filters;
833
- this.user = user;
834
- this.course = course;
835
- course.getCourseConfig().then((cfg) => {
836
- logger.debug(`[pipeline] Crated pipeline for ${cfg.name}`);
837
- }).catch((e) => {
838
- logger.error(`[pipeline] Failed to lookup courseCfg: ${e}`);
839
- });
840
- logPipelineConfig(generator, filters);
841
- }
842
- /**
843
- * Get weighted cards by running generator and applying filters.
844
- *
845
- * 1. Build shared context (user ELO, etc.)
846
- * 2. Get candidates from generator (passing context)
847
- * 3. Batch hydrate tags for all candidates
848
- * 4. Apply each filter sequentially
849
- * 5. Remove zero-score cards
850
- * 6. Sort by score descending
851
- * 7. Return top N
852
- *
853
- * @param limit - Maximum number of cards to return
854
- * @returns Cards sorted by score descending
855
- */
856
- async getWeightedCards(limit) {
857
- const context = await this.buildContext();
858
- const overFetchMultiplier = 2 + this.filters.length * 0.5;
859
- const fetchLimit = Math.ceil(limit * overFetchMultiplier);
860
- logger.debug(
861
- `[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
862
- );
863
- let cards = await this.generator.getWeightedCards(fetchLimit, context);
864
- const generatedCount = cards.length;
865
- logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
866
- cards = await this.hydrateTags(cards);
867
- for (const filter of this.filters) {
868
- const beforeCount = cards.length;
869
- cards = await filter.transform(cards, context);
870
- logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeCount} \u2192 ${cards.length} cards`);
871
- }
872
- cards = cards.filter((c) => c.score > 0);
873
- cards.sort((a, b) => b.score - a.score);
874
- const result = cards.slice(0, limit);
875
- const topScores = result.slice(0, 3).map((c) => c.score);
876
- logExecutionSummary(
877
- this.generator.name,
878
- generatedCount,
879
- this.filters.length,
880
- result.length,
881
- topScores
882
- );
883
- logCardProvenance(result, 3);
884
- return result;
885
- }
886
- /**
887
- * Batch hydrate tags for all cards.
888
- *
889
- * Fetches tags for all cards in a single database query and attaches them
890
- * to the WeightedCard objects. Filters can then use card.tags instead of
891
- * making individual getAppliedTags() calls.
892
- *
893
- * @param cards - Cards to hydrate
894
- * @returns Cards with tags populated
895
- */
896
- async hydrateTags(cards) {
897
- if (cards.length === 0) {
898
- return cards;
899
- }
900
- const cardIds = cards.map((c) => c.cardId);
901
- const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
902
- logTagHydration(cards, tagsByCard);
903
- return cards.map((card) => ({
904
- ...card,
905
- tags: tagsByCard.get(card.cardId) ?? []
906
- }));
907
- }
908
- /**
909
- * Build shared context for generator and filters.
910
- *
911
- * Called once per getWeightedCards() invocation.
912
- * Contains data that the generator and multiple filters might need.
913
- *
914
- * The context satisfies both GeneratorContext and FilterContext interfaces.
915
- */
916
- async buildContext() {
917
- let userElo = 1e3;
918
- try {
919
- const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
920
- const courseElo = (0, import_common5.toCourseElo)(courseReg.elo);
921
- userElo = courseElo.global.score;
922
- } catch (e) {
923
- logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
924
- }
925
- return {
926
- user: this.user,
927
- course: this.course,
928
- userElo
929
- };
930
- }
931
- /**
932
- * Get the course ID for this pipeline.
933
- */
934
- getCourseID() {
935
- return this.course.getCourseID();
936
- }
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);
936
+ static async fromStrategies(user, course, strategies, aggregationMode = DEFAULT_AGGREGATION_MODE) {
937
+ const generators = await Promise.all(
938
+ strategies.map((s) => ContentNavigator.create(user, course, s))
939
+ );
940
+ return new _CompositeGenerator(generators, aggregationMode);
976
941
  }
977
942
  /**
978
943
  * Get weighted cards from all generators, merge and deduplicate.
@@ -995,22 +960,55 @@ var init_CompositeGenerator = __esm({
995
960
  const results = await Promise.all(
996
961
  this.generators.map((g) => g.getWeightedCards(limit, context))
997
962
  );
963
+ const generatorSummaries = [];
964
+ results.forEach((cards, index) => {
965
+ const gen = this.generators[index];
966
+ const genName = gen.name || `Generator ${index}`;
967
+ const newCards = cards.filter((c) => c.provenance[0]?.reason?.includes("new card"));
968
+ const reviewCards = cards.filter((c) => c.provenance[0]?.reason?.includes("review"));
969
+ if (cards.length > 0) {
970
+ const topScore = Math.max(...cards.map((c) => c.score)).toFixed(2);
971
+ const parts = [];
972
+ if (newCards.length > 0) parts.push(`${newCards.length} new`);
973
+ if (reviewCards.length > 0) parts.push(`${reviewCards.length} reviews`);
974
+ const breakdown = parts.length > 0 ? parts.join(", ") : `${cards.length} cards`;
975
+ generatorSummaries.push(`${genName}: ${breakdown} (top: ${topScore})`);
976
+ } else {
977
+ generatorSummaries.push(`${genName}: 0 cards`);
978
+ }
979
+ });
980
+ logger.info(`[Composite] Generator breakdown: ${generatorSummaries.join(" | ")}`);
998
981
  const byCardId = /* @__PURE__ */ new Map();
999
- for (const cards of results) {
982
+ results.forEach((cards, index) => {
983
+ const gen = this.generators[index];
984
+ let weight = gen.learnable?.weight ?? 1;
985
+ let deviation;
986
+ if (gen.learnable && !gen.staticWeight && context.orchestration) {
987
+ const strategyId = gen.strategyId;
988
+ if (strategyId) {
989
+ weight = context.orchestration.getEffectiveWeight(strategyId, gen.learnable);
990
+ deviation = context.orchestration.getDeviation(strategyId);
991
+ }
992
+ }
1000
993
  for (const card of cards) {
994
+ if (card.provenance.length > 0) {
995
+ card.provenance[0].effectiveWeight = weight;
996
+ card.provenance[0].deviation = deviation;
997
+ }
1001
998
  const existing = byCardId.get(card.cardId) || [];
1002
- existing.push(card);
999
+ existing.push({ card, weight });
1003
1000
  byCardId.set(card.cardId, existing);
1004
1001
  }
1005
- }
1002
+ });
1006
1003
  const merged = [];
1007
- for (const [, cards] of byCardId) {
1008
- const aggregatedScore = this.aggregateScores(cards);
1004
+ for (const [, items] of byCardId) {
1005
+ const cards = items.map((i) => i.card);
1006
+ const aggregatedScore = this.aggregateScores(items);
1009
1007
  const finalScore = Math.min(1, aggregatedScore);
1010
1008
  const mergedProvenance = cards.flatMap((c) => c.provenance);
1011
1009
  const initialScore = cards[0].score;
1012
1010
  const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
1013
- const reason = this.buildAggregationReason(cards, finalScore);
1011
+ const reason = this.buildAggregationReason(items, finalScore);
1014
1012
  merged.push({
1015
1013
  ...cards[0],
1016
1014
  score: finalScore,
@@ -1032,22 +1030,26 @@ var init_CompositeGenerator = __esm({
1032
1030
  /**
1033
1031
  * Build human-readable reason for score aggregation.
1034
1032
  */
1035
- buildAggregationReason(cards, finalScore) {
1033
+ buildAggregationReason(items, finalScore) {
1034
+ const cards = items.map((i) => i.card);
1036
1035
  const count = cards.length;
1037
1036
  const scores = cards.map((c) => c.score.toFixed(2)).join(", ");
1038
1037
  if (count === 1) {
1039
- return `Single generator, score ${finalScore.toFixed(2)}`;
1038
+ const weightMsg = Math.abs(items[0].weight - 1) > 1e-3 ? ` (w=${items[0].weight.toFixed(2)})` : "";
1039
+ return `Single generator, score ${finalScore.toFixed(2)}${weightMsg}`;
1040
1040
  }
1041
1041
  const strategies = cards.map((c) => c.provenance[0]?.strategy || "unknown").join(", ");
1042
1042
  switch (this.aggregationMode) {
1043
1043
  case "max" /* MAX */:
1044
1044
  return `Max of ${count} generators (${strategies}): scores [${scores}] \u2192 ${finalScore.toFixed(2)}`;
1045
1045
  case "average" /* AVERAGE */:
1046
- return `Average of ${count} generators (${strategies}): scores [${scores}] \u2192 ${finalScore.toFixed(2)}`;
1046
+ return `Weighted Avg of ${count} generators (${strategies}): scores [${scores}] \u2192 ${finalScore.toFixed(2)}`;
1047
1047
  case "frequencyBoost" /* FREQUENCY_BOOST */: {
1048
- const avg = cards.reduce((sum, c) => sum + c.score, 0) / count;
1048
+ const totalWeight = items.reduce((sum, i) => sum + i.weight, 0);
1049
+ const weightedSum = items.reduce((sum, i) => sum + i.card.score * i.weight, 0);
1050
+ const avg = totalWeight > 0 ? weightedSum / totalWeight : 0;
1049
1051
  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)}`;
1052
+ return `Frequency boost from ${count} generators (${strategies}): w-avg ${avg.toFixed(2)} \xD7 ${boost.toFixed(2)} \u2192 ${finalScore.toFixed(2)}`;
1051
1053
  }
1052
1054
  default:
1053
1055
  return `Aggregated from ${count} generators: ${finalScore.toFixed(2)}`;
@@ -1056,16 +1058,22 @@ var init_CompositeGenerator = __esm({
1056
1058
  /**
1057
1059
  * Aggregate scores from multiple generators for the same card.
1058
1060
  */
1059
- aggregateScores(cards) {
1060
- const scores = cards.map((c) => c.score);
1061
+ aggregateScores(items) {
1062
+ const scores = items.map((i) => i.card.score);
1061
1063
  switch (this.aggregationMode) {
1062
1064
  case "max" /* MAX */:
1063
1065
  return Math.max(...scores);
1064
- case "average" /* AVERAGE */:
1065
- return scores.reduce((sum, s) => sum + s, 0) / scores.length;
1066
+ case "average" /* AVERAGE */: {
1067
+ const totalWeight = items.reduce((sum, i) => sum + i.weight, 0);
1068
+ if (totalWeight === 0) return 0;
1069
+ const weightedSum = items.reduce((sum, i) => sum + i.card.score * i.weight, 0);
1070
+ return weightedSum / totalWeight;
1071
+ }
1066
1072
  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);
1073
+ const totalWeight = items.reduce((sum, i) => sum + i.weight, 0);
1074
+ const weightedSum = items.reduce((sum, i) => sum + i.card.score * i.weight, 0);
1075
+ const avg = totalWeight > 0 ? weightedSum / totalWeight : 0;
1076
+ const frequencyBoost = 1 + FREQUENCY_BOOST_FACTOR * (items.length - 1);
1069
1077
  return avg * frequencyBoost;
1070
1078
  }
1071
1079
  default:
@@ -1076,134 +1084,18 @@ var init_CompositeGenerator = __esm({
1076
1084
  }
1077
1085
  });
1078
1086
 
1079
- // src/core/navigators/PipelineAssembler.ts
1080
- var PipelineAssembler;
1081
- var init_PipelineAssembler = __esm({
1082
- "src/core/navigators/PipelineAssembler.ts"() {
1083
- "use strict";
1084
- init_navigators();
1085
- init_Pipeline();
1086
- init_types_legacy();
1087
- init_logger();
1088
- init_CompositeGenerator();
1089
- PipelineAssembler = class {
1090
- /**
1091
- * Assembles a navigation pipeline from strategy documents.
1092
- *
1093
- * 1. Separates into generators and filters by role
1094
- * 2. Validates at least one generator exists (or creates default ELO)
1095
- * 3. Instantiates generators - wraps multiple in CompositeGenerator
1096
- * 4. Instantiates filters
1097
- * 5. Returns Pipeline(generator, filters)
1098
- *
1099
- * @param input - Strategy documents plus user/course interfaces
1100
- * @returns Assembled pipeline and any warnings
1101
- */
1102
- async assemble(input) {
1103
- const { strategies, user, course } = input;
1104
- const warnings = [];
1105
- if (strategies.length === 0) {
1106
- return {
1107
- pipeline: null,
1108
- generatorStrategies: [],
1109
- filterStrategies: [],
1110
- warnings
1111
- };
1112
- }
1113
- const generatorStrategies = [];
1114
- const filterStrategies = [];
1115
- for (const s of strategies) {
1116
- if (isGenerator(s.implementingClass)) {
1117
- generatorStrategies.push(s);
1118
- } else if (isFilter(s.implementingClass)) {
1119
- filterStrategies.push(s);
1120
- } else {
1121
- warnings.push(`Unknown strategy type '${s.implementingClass}', skipping: ${s.name}`);
1122
- }
1123
- }
1124
- if (generatorStrategies.length === 0) {
1125
- if (filterStrategies.length > 0) {
1126
- logger.debug(
1127
- "[PipelineAssembler] No generator found, using default ELO with configured filters"
1128
- );
1129
- generatorStrategies.push(this.makeDefaultEloStrategy(course.getCourseID()));
1130
- } else {
1131
- warnings.push("No generator strategy found");
1132
- return {
1133
- pipeline: null,
1134
- generatorStrategies: [],
1135
- filterStrategies: [],
1136
- warnings
1137
- };
1138
- }
1139
- }
1140
- let generator;
1141
- if (generatorStrategies.length === 1) {
1142
- const nav = await ContentNavigator.create(user, course, generatorStrategies[0]);
1143
- generator = nav;
1144
- logger.debug(`[PipelineAssembler] Using single generator: ${generatorStrategies[0].name}`);
1145
- } 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
1087
  // src/core/navigators/generators/elo.ts
1201
- var import_common6, ELONavigator;
1088
+ var elo_exports = {};
1089
+ __export(elo_exports, {
1090
+ default: () => ELONavigator
1091
+ });
1092
+ var import_common5, ELONavigator;
1202
1093
  var init_elo = __esm({
1203
1094
  "src/core/navigators/generators/elo.ts"() {
1204
1095
  "use strict";
1205
1096
  init_navigators();
1206
- import_common6 = require("@vue-skuilder/common");
1097
+ import_common5 = require("@vue-skuilder/common");
1098
+ init_logger();
1207
1099
  ELONavigator = class extends ContentNavigator {
1208
1100
  /** Human-readable name for CardGenerator interface */
1209
1101
  name;
@@ -1232,7 +1124,7 @@ var init_elo = __esm({
1232
1124
  userGlobalElo = context.userElo;
1233
1125
  } else {
1234
1126
  const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
1235
- const userElo = (0, import_common6.toCourseElo)(courseReg.elo);
1127
+ const userElo = (0, import_common5.toCourseElo)(courseReg.elo);
1236
1128
  userGlobalElo = userElo.global.score;
1237
1129
  }
1238
1130
  const activeCards = await this.user.getActiveCards();
@@ -1263,199 +1155,1955 @@ var init_elo = __esm({
1263
1155
  };
1264
1156
  });
1265
1157
  scored.sort((a, b) => b.score - a.score);
1266
- return scored.slice(0, limit);
1267
- }
1268
- };
1269
- }
1158
+ const result = scored.slice(0, limit);
1159
+ if (result.length > 0) {
1160
+ const topScores = result.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ");
1161
+ logger.info(
1162
+ `[ELO] Course ${this.course.getCourseID()}: ${result.length} new cards (top scores: ${topScores})`
1163
+ );
1164
+ } else {
1165
+ logger.info(`[ELO] Course ${this.course.getCourseID()}: No new cards available`);
1166
+ }
1167
+ return result;
1168
+ }
1169
+ };
1170
+ }
1171
+ });
1172
+
1173
+ // src/core/navigators/generators/index.ts
1174
+ var generators_exports = {};
1175
+ var init_generators = __esm({
1176
+ "src/core/navigators/generators/index.ts"() {
1177
+ "use strict";
1178
+ }
1270
1179
  });
1271
1180
 
1272
1181
  // src/core/navigators/generators/srs.ts
1273
- var import_moment, SRSNavigator;
1182
+ var srs_exports = {};
1183
+ __export(srs_exports, {
1184
+ default: () => SRSNavigator
1185
+ });
1186
+ var import_moment, DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_PRESSURE, SRSNavigator;
1274
1187
  var init_srs = __esm({
1275
1188
  "src/core/navigators/generators/srs.ts"() {
1276
1189
  "use strict";
1277
1190
  import_moment = __toESM(require("moment"), 1);
1278
1191
  init_navigators();
1279
1192
  init_logger();
1193
+ DEFAULT_HEALTHY_BACKLOG = 20;
1194
+ MAX_BACKLOG_PRESSURE = 0.5;
1280
1195
  SRSNavigator = class extends ContentNavigator {
1281
1196
  /** Human-readable name for CardGenerator interface */
1282
1197
  name;
1198
+ /** Healthy backlog threshold - when exceeded, backlog pressure kicks in */
1199
+ healthyBacklog;
1283
1200
  constructor(user, course, strategyData) {
1284
1201
  super(user, course, strategyData);
1285
1202
  this.name = strategyData?.name || "SRS";
1203
+ const config = this.parseConfig(strategyData?.serializedData);
1204
+ this.healthyBacklog = config.healthyBacklog ?? DEFAULT_HEALTHY_BACKLOG;
1205
+ }
1206
+ /**
1207
+ * Parse configuration from serialized JSON.
1208
+ */
1209
+ parseConfig(serializedData) {
1210
+ if (!serializedData) return {};
1211
+ try {
1212
+ return JSON.parse(serializedData);
1213
+ } catch {
1214
+ logger.warn("[SRS] Failed to parse strategy config, using defaults");
1215
+ return {};
1216
+ }
1217
+ }
1218
+ /**
1219
+ * Get review cards scored by urgency.
1220
+ *
1221
+ * Score formula combines:
1222
+ * - Relative overdueness: hoursOverdue / intervalHours
1223
+ * - Interval recency: exponential decay favoring shorter intervals
1224
+ * - Backlog pressure: boost when due reviews exceed healthy threshold
1225
+ *
1226
+ * Cards not yet due are excluded (not scored as 0).
1227
+ *
1228
+ * This method supports both the legacy signature (limit only) and the
1229
+ * CardGenerator interface signature (limit, context).
1230
+ *
1231
+ * @param limit - Maximum number of cards to return
1232
+ * @param _context - Optional GeneratorContext (currently unused, but required for interface)
1233
+ */
1234
+ async getWeightedCards(limit, _context) {
1235
+ if (!this.user || !this.course) {
1236
+ throw new Error("SRSNavigator requires user and course to be set");
1237
+ }
1238
+ const courseId = this.course.getCourseID();
1239
+ const reviews = await this.user.getPendingReviews(courseId);
1240
+ const now = import_moment.default.utc();
1241
+ const dueReviews = reviews.filter((r) => now.isAfter(import_moment.default.utc(r.reviewTime)));
1242
+ const backlogPressure = this.computeBacklogPressure(dueReviews.length);
1243
+ if (dueReviews.length > 0) {
1244
+ const pressureNote = backlogPressure > 0 ? ` [backlog pressure: +${backlogPressure.toFixed(2)}]` : ` [healthy backlog]`;
1245
+ logger.info(
1246
+ `[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
1247
+ );
1248
+ } else if (reviews.length > 0) {
1249
+ const sortedByDue = [...reviews].sort(
1250
+ (a, b) => import_moment.default.utc(a.reviewTime).diff(import_moment.default.utc(b.reviewTime))
1251
+ );
1252
+ const nextDue = sortedByDue[0];
1253
+ const nextDueTime = import_moment.default.utc(nextDue.reviewTime);
1254
+ const untilDue = import_moment.default.duration(nextDueTime.diff(now));
1255
+ const untilDueStr = untilDue.asHours() < 1 ? `${Math.round(untilDue.asMinutes())}m` : untilDue.asHours() < 24 ? `${Math.round(untilDue.asHours())}h` : `${Math.round(untilDue.asDays())}d`;
1256
+ logger.info(
1257
+ `[SRS] Course ${courseId}: 0 reviews due now (${reviews.length} scheduled, next in ${untilDueStr})`
1258
+ );
1259
+ } else {
1260
+ logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
1261
+ }
1262
+ const scored = dueReviews.map((review) => {
1263
+ const { score, reason } = this.computeUrgencyScore(review, now, backlogPressure);
1264
+ return {
1265
+ cardId: review.cardId,
1266
+ courseId: review.courseId,
1267
+ score,
1268
+ reviewID: review._id,
1269
+ provenance: [
1270
+ {
1271
+ strategy: "srs",
1272
+ strategyName: this.strategyName || this.name,
1273
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-SRS-default",
1274
+ action: "generated",
1275
+ score,
1276
+ reason
1277
+ }
1278
+ ]
1279
+ };
1280
+ });
1281
+ return scored.sort((a, b) => b.score - a.score).slice(0, limit);
1282
+ }
1283
+ /**
1284
+ * Compute backlog pressure based on number of due reviews.
1285
+ *
1286
+ * Backlog pressure is 0 when at or below healthy threshold,
1287
+ * and increases linearly above it, maxing out at MAX_BACKLOG_PRESSURE.
1288
+ *
1289
+ * Examples (with default healthyBacklog=20):
1290
+ * - 10 due reviews → 0.00 (healthy)
1291
+ * - 20 due reviews → 0.00 (at threshold)
1292
+ * - 40 due reviews → 0.25 (2x threshold)
1293
+ * - 60 due reviews → 0.50 (3x threshold, maxed)
1294
+ *
1295
+ * @param dueCount - Number of reviews currently due
1296
+ * @returns Backlog pressure score to add to urgency (0 to MAX_BACKLOG_PRESSURE)
1297
+ */
1298
+ computeBacklogPressure(dueCount) {
1299
+ if (dueCount <= this.healthyBacklog) {
1300
+ return 0;
1301
+ }
1302
+ const excess = dueCount - this.healthyBacklog;
1303
+ const pressure = excess / this.healthyBacklog * (MAX_BACKLOG_PRESSURE / 2);
1304
+ return Math.min(MAX_BACKLOG_PRESSURE, pressure);
1305
+ }
1306
+ /**
1307
+ * Compute urgency score for a review card.
1308
+ *
1309
+ * Three factors:
1310
+ * 1. Relative overdueness = hoursOverdue / intervalHours
1311
+ * - 2 days overdue on 3-day interval = 0.67 (urgent)
1312
+ * - 2 days overdue on 180-day interval = 0.01 (not urgent)
1313
+ *
1314
+ * 2. Interval recency factor = 0.3 + 0.7 * exp(-intervalHours / 720)
1315
+ * - 24h interval → ~1.0 (very recent learning)
1316
+ * - 30 days (720h) → ~0.56
1317
+ * - 180 days → ~0.30
1318
+ *
1319
+ * 3. Backlog pressure = global boost when review backlog exceeds healthy threshold
1320
+ * - At healthy backlog: 0
1321
+ * - At 2x healthy: +0.25
1322
+ * - At 3x+ healthy: +0.50 (max)
1323
+ *
1324
+ * Combined: base 0.5 + (urgency factors * 0.45) + backlog pressure
1325
+ * Result range: 0.5 to 1.0 (uncapped to allow high-urgency reviews to compete with new cards)
1326
+ *
1327
+ * @param review - The scheduled card to score
1328
+ * @param now - Current time
1329
+ * @param backlogPressure - Pre-computed backlog pressure (0 to 0.5)
1330
+ */
1331
+ computeUrgencyScore(review, now, backlogPressure) {
1332
+ const scheduledAt = import_moment.default.utc(review.scheduledAt);
1333
+ const due = import_moment.default.utc(review.reviewTime);
1334
+ const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
1335
+ const hoursOverdue = now.diff(due, "hours");
1336
+ const relativeOverdue = hoursOverdue / intervalHours;
1337
+ const recencyFactor = 0.3 + 0.7 * Math.exp(-intervalHours / 720);
1338
+ const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
1339
+ const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
1340
+ const baseScore = 0.5 + urgency * 0.45;
1341
+ const score = Math.min(1, baseScore + backlogPressure);
1342
+ const reasonParts = [
1343
+ `${Math.round(hoursOverdue)}h overdue`,
1344
+ `interval: ${Math.round(intervalHours)}h`,
1345
+ `relative: ${relativeOverdue.toFixed(2)}`,
1346
+ `recency: ${recencyFactor.toFixed(2)}`
1347
+ ];
1348
+ if (backlogPressure > 0) {
1349
+ reasonParts.push(`backlog: +${backlogPressure.toFixed(2)}`);
1350
+ }
1351
+ reasonParts.push("review");
1352
+ const reason = reasonParts.join(", ");
1353
+ return { score, reason };
1354
+ }
1355
+ };
1356
+ }
1357
+ });
1358
+
1359
+ // src/core/navigators/generators/types.ts
1360
+ var types_exports = {};
1361
+ var init_types = __esm({
1362
+ "src/core/navigators/generators/types.ts"() {
1363
+ "use strict";
1364
+ }
1365
+ });
1366
+
1367
+ // import("./generators/**/*") in src/core/navigators/index.ts
1368
+ var globImport_generators;
1369
+ var init_ = __esm({
1370
+ 'import("./generators/**/*") in src/core/navigators/index.ts'() {
1371
+ globImport_generators = __glob({
1372
+ "./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
1373
+ "./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
1374
+ "./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
1375
+ "./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
1376
+ "./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports))
1377
+ });
1378
+ }
1379
+ });
1380
+
1381
+ // src/core/types/contentNavigationStrategy.ts
1382
+ var DEFAULT_LEARNABLE_WEIGHT;
1383
+ var init_contentNavigationStrategy = __esm({
1384
+ "src/core/types/contentNavigationStrategy.ts"() {
1385
+ "use strict";
1386
+ DEFAULT_LEARNABLE_WEIGHT = {
1387
+ weight: 1,
1388
+ confidence: 0.1,
1389
+ // Low confidence initially = wide exploration
1390
+ sampleSize: 0
1391
+ };
1392
+ }
1393
+ });
1394
+
1395
+ // src/core/navigators/filters/WeightedFilter.ts
1396
+ var WeightedFilter_exports = {};
1397
+ __export(WeightedFilter_exports, {
1398
+ WeightedFilter: () => WeightedFilter
1399
+ });
1400
+ var WeightedFilter;
1401
+ var init_WeightedFilter = __esm({
1402
+ "src/core/navigators/filters/WeightedFilter.ts"() {
1403
+ "use strict";
1404
+ init_contentNavigationStrategy();
1405
+ WeightedFilter = class {
1406
+ name;
1407
+ inner;
1408
+ learnable;
1409
+ staticWeight;
1410
+ strategyId;
1411
+ constructor(inner, learnable = DEFAULT_LEARNABLE_WEIGHT, staticWeight = false, strategyId) {
1412
+ this.inner = inner;
1413
+ this.name = inner.name;
1414
+ this.learnable = learnable;
1415
+ this.staticWeight = staticWeight;
1416
+ this.strategyId = strategyId;
1417
+ }
1418
+ /**
1419
+ * Apply the inner filter, then scale its effect by the configured weight.
1420
+ */
1421
+ async transform(cards, context) {
1422
+ let effectiveWeight = this.learnable.weight;
1423
+ let deviation;
1424
+ if (!this.staticWeight && context.orchestration) {
1425
+ const strategyId = this.strategyId || this.inner.strategyId || this.name;
1426
+ effectiveWeight = context.orchestration.getEffectiveWeight(strategyId, this.learnable);
1427
+ deviation = context.orchestration.getDeviation(strategyId);
1428
+ }
1429
+ if (Math.abs(effectiveWeight - 1) < 1e-3) {
1430
+ return this.inner.transform(cards, context);
1431
+ }
1432
+ const originalScores = /* @__PURE__ */ new Map();
1433
+ for (const card of cards) {
1434
+ originalScores.set(card.cardId, card.score);
1435
+ }
1436
+ const transformedCards = await this.inner.transform(cards, context);
1437
+ return transformedCards.map((card) => {
1438
+ const originalScore = originalScores.get(card.cardId);
1439
+ if (originalScore === void 0 || originalScore === 0 || card.score === 0) {
1440
+ return card;
1441
+ }
1442
+ const rawEffect = card.score / originalScore;
1443
+ if (Math.abs(rawEffect - 1) < 1e-4) {
1444
+ return card;
1445
+ }
1446
+ const weightedEffect = Math.pow(rawEffect, effectiveWeight);
1447
+ const newScore = originalScore * weightedEffect;
1448
+ const lastProvIndex = card.provenance.length - 1;
1449
+ const lastProv = card.provenance[lastProvIndex];
1450
+ if (lastProv) {
1451
+ const updatedProvenance = [...card.provenance];
1452
+ updatedProvenance[lastProvIndex] = {
1453
+ ...lastProv,
1454
+ score: newScore,
1455
+ effectiveWeight,
1456
+ deviation
1457
+ // We can optionally append to the reason, but the structured field is key
1458
+ };
1459
+ return {
1460
+ ...card,
1461
+ score: newScore,
1462
+ provenance: updatedProvenance
1463
+ };
1464
+ }
1465
+ return {
1466
+ ...card,
1467
+ score: newScore
1468
+ };
1469
+ });
1470
+ }
1471
+ };
1472
+ }
1473
+ });
1474
+
1475
+ // src/core/navigators/filters/eloDistance.ts
1476
+ var eloDistance_exports = {};
1477
+ __export(eloDistance_exports, {
1478
+ DEFAULT_HALF_LIFE: () => DEFAULT_HALF_LIFE,
1479
+ DEFAULT_MAX_MULTIPLIER: () => DEFAULT_MAX_MULTIPLIER,
1480
+ DEFAULT_MIN_MULTIPLIER: () => DEFAULT_MIN_MULTIPLIER,
1481
+ createEloDistanceFilter: () => createEloDistanceFilter
1482
+ });
1483
+ function computeMultiplier(distance, halfLife, minMultiplier, maxMultiplier) {
1484
+ const normalizedDistance = distance / halfLife;
1485
+ const decay = Math.exp(-(normalizedDistance * normalizedDistance));
1486
+ return minMultiplier + (maxMultiplier - minMultiplier) * decay;
1487
+ }
1488
+ function createEloDistanceFilter(config) {
1489
+ const halfLife = config?.halfLife ?? DEFAULT_HALF_LIFE;
1490
+ const minMultiplier = config?.minMultiplier ?? DEFAULT_MIN_MULTIPLIER;
1491
+ const maxMultiplier = config?.maxMultiplier ?? DEFAULT_MAX_MULTIPLIER;
1492
+ return {
1493
+ name: "ELO Distance Filter",
1494
+ async transform(cards, context) {
1495
+ const { course, userElo } = context;
1496
+ const cardIds = cards.map((c) => c.cardId);
1497
+ const cardElos = await course.getCardEloData(cardIds);
1498
+ return cards.map((card, i) => {
1499
+ const cardElo = cardElos[i]?.global?.score ?? 1e3;
1500
+ const distance = Math.abs(cardElo - userElo);
1501
+ const multiplier = computeMultiplier(distance, halfLife, minMultiplier, maxMultiplier);
1502
+ const newScore = card.score * multiplier;
1503
+ const action = multiplier < maxMultiplier - 0.01 ? "penalized" : "passed";
1504
+ return {
1505
+ ...card,
1506
+ score: newScore,
1507
+ provenance: [
1508
+ ...card.provenance,
1509
+ {
1510
+ strategy: "eloDistance",
1511
+ strategyName: "ELO Distance Filter",
1512
+ strategyId: "ELO_DISTANCE_FILTER",
1513
+ action,
1514
+ score: newScore,
1515
+ reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userElo)}) \u2192 ${multiplier.toFixed(2)}x`
1516
+ }
1517
+ ]
1518
+ };
1519
+ });
1520
+ }
1521
+ };
1522
+ }
1523
+ var DEFAULT_HALF_LIFE, DEFAULT_MIN_MULTIPLIER, DEFAULT_MAX_MULTIPLIER;
1524
+ var init_eloDistance = __esm({
1525
+ "src/core/navigators/filters/eloDistance.ts"() {
1526
+ "use strict";
1527
+ DEFAULT_HALF_LIFE = 200;
1528
+ DEFAULT_MIN_MULTIPLIER = 0.3;
1529
+ DEFAULT_MAX_MULTIPLIER = 1;
1530
+ }
1531
+ });
1532
+
1533
+ // src/core/navigators/filters/hierarchyDefinition.ts
1534
+ var hierarchyDefinition_exports = {};
1535
+ __export(hierarchyDefinition_exports, {
1536
+ default: () => HierarchyDefinitionNavigator
1537
+ });
1538
+ var import_common6, DEFAULT_MIN_COUNT, HierarchyDefinitionNavigator;
1539
+ var init_hierarchyDefinition = __esm({
1540
+ "src/core/navigators/filters/hierarchyDefinition.ts"() {
1541
+ "use strict";
1542
+ init_navigators();
1543
+ import_common6 = require("@vue-skuilder/common");
1544
+ DEFAULT_MIN_COUNT = 3;
1545
+ HierarchyDefinitionNavigator = class extends ContentNavigator {
1546
+ config;
1547
+ /** Human-readable name for CardFilter interface */
1548
+ name;
1549
+ constructor(user, course, strategyData) {
1550
+ super(user, course, strategyData);
1551
+ this.config = this.parseConfig(strategyData.serializedData);
1552
+ this.name = strategyData.name || "Hierarchy Definition";
1553
+ }
1554
+ parseConfig(serializedData) {
1555
+ try {
1556
+ const parsed = JSON.parse(serializedData);
1557
+ return {
1558
+ prerequisites: parsed.prerequisites || {}
1559
+ };
1560
+ } catch {
1561
+ return {
1562
+ prerequisites: {}
1563
+ };
1564
+ }
1565
+ }
1566
+ /**
1567
+ * Check if a specific prerequisite is satisfied
1568
+ */
1569
+ isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
1570
+ if (!userTagElo) return false;
1571
+ const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
1572
+ if (userTagElo.count < minCount) return false;
1573
+ if (prereq.masteryThreshold?.minElo !== void 0) {
1574
+ return userTagElo.score >= prereq.masteryThreshold.minElo;
1575
+ } else {
1576
+ return userTagElo.score >= userGlobalElo;
1577
+ }
1578
+ }
1579
+ /**
1580
+ * Get the set of tags the user has mastered.
1581
+ * A tag is "mastered" if it appears as a prerequisite somewhere and meets its threshold.
1582
+ */
1583
+ async getMasteredTags(context) {
1584
+ const mastered = /* @__PURE__ */ new Set();
1585
+ try {
1586
+ const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
1587
+ const userElo = (0, import_common6.toCourseElo)(courseReg.elo);
1588
+ for (const prereqs of Object.values(this.config.prerequisites)) {
1589
+ for (const prereq of prereqs) {
1590
+ const tagElo = userElo.tags[prereq.tag];
1591
+ if (this.isPrerequisiteMet(prereq, tagElo, userElo.global.score)) {
1592
+ mastered.add(prereq.tag);
1593
+ }
1594
+ }
1595
+ }
1596
+ } catch {
1597
+ }
1598
+ return mastered;
1599
+ }
1600
+ /**
1601
+ * Get the set of tags that are unlocked (prerequisites met)
1602
+ */
1603
+ getUnlockedTags(masteredTags) {
1604
+ const unlocked = /* @__PURE__ */ new Set();
1605
+ for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
1606
+ const allPrereqsMet = prereqs.every((prereq) => masteredTags.has(prereq.tag));
1607
+ if (allPrereqsMet) {
1608
+ unlocked.add(tagId);
1609
+ }
1610
+ }
1611
+ return unlocked;
1612
+ }
1613
+ /**
1614
+ * Check if a tag has prerequisites defined in config
1615
+ */
1616
+ hasPrerequisites(tagId) {
1617
+ return tagId in this.config.prerequisites;
1618
+ }
1619
+ /**
1620
+ * Check if a card is unlocked and generate reason.
1621
+ */
1622
+ async checkCardUnlock(card, _course, unlockedTags, masteredTags) {
1623
+ try {
1624
+ const cardTags = card.tags ?? [];
1625
+ const lockedTags = cardTags.filter(
1626
+ (tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
1627
+ );
1628
+ if (lockedTags.length === 0) {
1629
+ const tagList = cardTags.length > 0 ? cardTags.join(", ") : "none";
1630
+ return {
1631
+ isUnlocked: true,
1632
+ reason: `Prerequisites met, tags: ${tagList}`
1633
+ };
1634
+ }
1635
+ const missingPrereqs = lockedTags.flatMap((tag) => {
1636
+ const prereqs = this.config.prerequisites[tag] || [];
1637
+ return prereqs.filter((p) => !masteredTags.has(p.tag)).map((p) => p.tag);
1638
+ });
1639
+ return {
1640
+ isUnlocked: false,
1641
+ reason: `Blocked: missing prerequisites ${missingPrereqs.join(", ")} for tags ${lockedTags.join(", ")}`
1642
+ };
1643
+ } catch {
1644
+ return {
1645
+ isUnlocked: true,
1646
+ reason: "Prerequisites check skipped (tag lookup failed)"
1647
+ };
1648
+ }
1649
+ }
1650
+ /**
1651
+ * CardFilter.transform implementation.
1652
+ *
1653
+ * Apply prerequisite gating to cards. Cards with locked tags receive score: 0.
1654
+ */
1655
+ async transform(cards, context) {
1656
+ const masteredTags = await this.getMasteredTags(context);
1657
+ const unlockedTags = this.getUnlockedTags(masteredTags);
1658
+ const gated = [];
1659
+ for (const card of cards) {
1660
+ const { isUnlocked, reason } = await this.checkCardUnlock(
1661
+ card,
1662
+ context.course,
1663
+ unlockedTags,
1664
+ masteredTags
1665
+ );
1666
+ const finalScore = isUnlocked ? card.score : 0;
1667
+ const action = isUnlocked ? "passed" : "penalized";
1668
+ gated.push({
1669
+ ...card,
1670
+ score: finalScore,
1671
+ provenance: [
1672
+ ...card.provenance,
1673
+ {
1674
+ strategy: "hierarchyDefinition",
1675
+ strategyName: this.strategyName || this.name,
1676
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-hierarchy",
1677
+ action,
1678
+ score: finalScore,
1679
+ reason
1680
+ }
1681
+ ]
1682
+ });
1683
+ }
1684
+ return gated;
1685
+ }
1686
+ /**
1687
+ * Legacy getWeightedCards - now throws as filters should not be used as generators.
1688
+ *
1689
+ * Use transform() via Pipeline instead.
1690
+ */
1691
+ async getWeightedCards(_limit) {
1692
+ throw new Error(
1693
+ "HierarchyDefinitionNavigator is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1694
+ );
1695
+ }
1696
+ };
1697
+ }
1698
+ });
1699
+
1700
+ // src/core/navigators/filters/userTagPreference.ts
1701
+ var userTagPreference_exports = {};
1702
+ __export(userTagPreference_exports, {
1703
+ default: () => UserTagPreferenceFilter
1704
+ });
1705
+ var UserTagPreferenceFilter;
1706
+ var init_userTagPreference = __esm({
1707
+ "src/core/navigators/filters/userTagPreference.ts"() {
1708
+ "use strict";
1709
+ init_navigators();
1710
+ UserTagPreferenceFilter = class extends ContentNavigator {
1711
+ _strategyData;
1712
+ /** Human-readable name for CardFilter interface */
1713
+ name;
1714
+ constructor(user, course, strategyData) {
1715
+ super(user, course, strategyData);
1716
+ this._strategyData = strategyData;
1717
+ this.name = strategyData.name || "User Tag Preferences";
1718
+ }
1719
+ /**
1720
+ * Compute multiplier for a card based on its tags and user preferences.
1721
+ * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
1722
+ */
1723
+ computeMultiplier(cardTags, boostMap) {
1724
+ const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
1725
+ if (multipliers.length === 0) {
1726
+ return 1;
1727
+ }
1728
+ return Math.max(...multipliers);
1729
+ }
1730
+ /**
1731
+ * Build human-readable reason for the filter's decision.
1732
+ */
1733
+ buildReason(cardTags, boostMap, multiplier) {
1734
+ const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
1735
+ if (multiplier === 0) {
1736
+ return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
1737
+ }
1738
+ if (multiplier < 1) {
1739
+ return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1740
+ }
1741
+ if (multiplier > 1) {
1742
+ return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
1743
+ }
1744
+ return "No matching user preferences";
1745
+ }
1746
+ /**
1747
+ * CardFilter.transform implementation.
1748
+ *
1749
+ * Apply user tag preferences:
1750
+ * 1. Read preferences from strategy state
1751
+ * 2. If no preferences, pass through unchanged
1752
+ * 3. For each card:
1753
+ * - Look up tag in boost record
1754
+ * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
1755
+ * - If multiple tags match: use max multiplier
1756
+ * - Append provenance with clear reason
1757
+ */
1758
+ async transform(cards, _context) {
1759
+ const prefs = await this.getStrategyState();
1760
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
1761
+ return cards.map((card) => ({
1762
+ ...card,
1763
+ provenance: [
1764
+ ...card.provenance,
1765
+ {
1766
+ strategy: "userTagPreference",
1767
+ strategyName: this.strategyName || this.name,
1768
+ strategyId: this.strategyId || this._strategyData._id,
1769
+ action: "passed",
1770
+ score: card.score,
1771
+ reason: "No user tag preferences configured"
1772
+ }
1773
+ ]
1774
+ }));
1775
+ }
1776
+ const adjusted = await Promise.all(
1777
+ cards.map(async (card) => {
1778
+ const cardTags = card.tags ?? [];
1779
+ const multiplier = this.computeMultiplier(cardTags, prefs.boost);
1780
+ const finalScore = Math.min(1, card.score * multiplier);
1781
+ let action;
1782
+ if (multiplier === 0 || multiplier < 1) {
1783
+ action = "penalized";
1784
+ } else if (multiplier > 1) {
1785
+ action = "boosted";
1786
+ } else {
1787
+ action = "passed";
1788
+ }
1789
+ return {
1790
+ ...card,
1791
+ score: finalScore,
1792
+ provenance: [
1793
+ ...card.provenance,
1794
+ {
1795
+ strategy: "userTagPreference",
1796
+ strategyName: this.strategyName || this.name,
1797
+ strategyId: this.strategyId || this._strategyData._id,
1798
+ action,
1799
+ score: finalScore,
1800
+ reason: this.buildReason(cardTags, prefs.boost, multiplier)
1801
+ }
1802
+ ]
1803
+ };
1804
+ })
1805
+ );
1806
+ return adjusted;
1807
+ }
1808
+ /**
1809
+ * Legacy getWeightedCards - throws as filters should not be used as generators.
1810
+ */
1811
+ async getWeightedCards(_limit) {
1812
+ throw new Error(
1813
+ "UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
1814
+ );
1815
+ }
1816
+ };
1817
+ }
1818
+ });
1819
+
1820
+ // src/core/navigators/filters/index.ts
1821
+ var filters_exports = {};
1822
+ __export(filters_exports, {
1823
+ UserTagPreferenceFilter: () => UserTagPreferenceFilter,
1824
+ createEloDistanceFilter: () => createEloDistanceFilter
1825
+ });
1826
+ var init_filters = __esm({
1827
+ "src/core/navigators/filters/index.ts"() {
1828
+ "use strict";
1829
+ init_eloDistance();
1830
+ init_userTagPreference();
1831
+ }
1832
+ });
1833
+
1834
+ // src/core/navigators/filters/inferredPreferenceStub.ts
1835
+ var inferredPreferenceStub_exports = {};
1836
+ __export(inferredPreferenceStub_exports, {
1837
+ INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
1838
+ });
1839
+ var INFERRED_PREFERENCE_NAVIGATOR_STUB;
1840
+ var init_inferredPreferenceStub = __esm({
1841
+ "src/core/navigators/filters/inferredPreferenceStub.ts"() {
1842
+ "use strict";
1843
+ INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
1844
+ }
1845
+ });
1846
+
1847
+ // src/core/navigators/filters/interferenceMitigator.ts
1848
+ var interferenceMitigator_exports = {};
1849
+ __export(interferenceMitigator_exports, {
1850
+ default: () => InterferenceMitigatorNavigator
1851
+ });
1852
+ var import_common7, DEFAULT_MIN_COUNT2, DEFAULT_MIN_ELAPSED_DAYS, DEFAULT_INTERFERENCE_DECAY, InterferenceMitigatorNavigator;
1853
+ var init_interferenceMitigator = __esm({
1854
+ "src/core/navigators/filters/interferenceMitigator.ts"() {
1855
+ "use strict";
1856
+ init_navigators();
1857
+ import_common7 = require("@vue-skuilder/common");
1858
+ DEFAULT_MIN_COUNT2 = 10;
1859
+ DEFAULT_MIN_ELAPSED_DAYS = 3;
1860
+ DEFAULT_INTERFERENCE_DECAY = 0.8;
1861
+ InterferenceMitigatorNavigator = class extends ContentNavigator {
1862
+ config;
1863
+ /** Human-readable name for CardFilter interface */
1864
+ name;
1865
+ /** Precomputed map: tag -> set of { partner, decay } it interferes with */
1866
+ interferenceMap;
1867
+ constructor(user, course, strategyData) {
1868
+ super(user, course, strategyData);
1869
+ this.config = this.parseConfig(strategyData.serializedData);
1870
+ this.interferenceMap = this.buildInterferenceMap();
1871
+ this.name = strategyData.name || "Interference Mitigator";
1872
+ }
1873
+ parseConfig(serializedData) {
1874
+ try {
1875
+ const parsed = JSON.parse(serializedData);
1876
+ let sets = parsed.interferenceSets || [];
1877
+ if (sets.length > 0 && Array.isArray(sets[0])) {
1878
+ sets = sets.map((tags) => ({ tags }));
1879
+ }
1880
+ return {
1881
+ interferenceSets: sets,
1882
+ maturityThreshold: {
1883
+ minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT2,
1884
+ minElo: parsed.maturityThreshold?.minElo,
1885
+ minElapsedDays: parsed.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS
1886
+ },
1887
+ defaultDecay: parsed.defaultDecay ?? DEFAULT_INTERFERENCE_DECAY
1888
+ };
1889
+ } catch {
1890
+ return {
1891
+ interferenceSets: [],
1892
+ maturityThreshold: {
1893
+ minCount: DEFAULT_MIN_COUNT2,
1894
+ minElapsedDays: DEFAULT_MIN_ELAPSED_DAYS
1895
+ },
1896
+ defaultDecay: DEFAULT_INTERFERENCE_DECAY
1897
+ };
1898
+ }
1899
+ }
1900
+ /**
1901
+ * Build a map from each tag to its interference partners with decay coefficients.
1902
+ * If tags A, B, C are in an interference group with decay 0.8, then:
1903
+ * - A interferes with B (decay 0.8) and C (decay 0.8)
1904
+ * - B interferes with A (decay 0.8) and C (decay 0.8)
1905
+ * - etc.
1906
+ */
1907
+ buildInterferenceMap() {
1908
+ const map = /* @__PURE__ */ new Map();
1909
+ for (const group of this.config.interferenceSets) {
1910
+ const decay = group.decay ?? this.config.defaultDecay ?? DEFAULT_INTERFERENCE_DECAY;
1911
+ for (const tag of group.tags) {
1912
+ if (!map.has(tag)) {
1913
+ map.set(tag, []);
1914
+ }
1915
+ const partners = map.get(tag);
1916
+ for (const other of group.tags) {
1917
+ if (other !== tag) {
1918
+ const existing = partners.find((p) => p.partner === other);
1919
+ if (existing) {
1920
+ existing.decay = Math.max(existing.decay, decay);
1921
+ } else {
1922
+ partners.push({ partner: other, decay });
1923
+ }
1924
+ }
1925
+ }
1926
+ }
1927
+ }
1928
+ return map;
1929
+ }
1930
+ /**
1931
+ * Get the set of tags that are currently immature for this user.
1932
+ * A tag is immature if the user has interacted with it but hasn't
1933
+ * reached the maturity threshold.
1934
+ */
1935
+ async getImmatureTags(context) {
1936
+ const immature = /* @__PURE__ */ new Set();
1937
+ try {
1938
+ const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
1939
+ const userElo = (0, import_common7.toCourseElo)(courseReg.elo);
1940
+ const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT2;
1941
+ const minElo = this.config.maturityThreshold?.minElo;
1942
+ const minElapsedDays = this.config.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS;
1943
+ const minCountForElapsed = minElapsedDays * 2;
1944
+ for (const [tagId, tagElo] of Object.entries(userElo.tags)) {
1945
+ if (tagElo.count === 0) continue;
1946
+ const belowCount = tagElo.count < minCount;
1947
+ const belowElo = minElo !== void 0 && tagElo.score < minElo;
1948
+ const belowElapsed = tagElo.count < minCountForElapsed;
1949
+ if (belowCount || belowElo || belowElapsed) {
1950
+ immature.add(tagId);
1951
+ }
1952
+ }
1953
+ } catch {
1954
+ }
1955
+ return immature;
1956
+ }
1957
+ /**
1958
+ * Get all tags that interfere with any immature tag, along with their decay coefficients.
1959
+ * These are the tags we want to avoid introducing.
1960
+ */
1961
+ getTagsToAvoid(immatureTags) {
1962
+ const avoid = /* @__PURE__ */ new Map();
1963
+ for (const immatureTag of immatureTags) {
1964
+ const partners = this.interferenceMap.get(immatureTag);
1965
+ if (partners) {
1966
+ for (const { partner, decay } of partners) {
1967
+ if (!immatureTags.has(partner)) {
1968
+ const existing = avoid.get(partner) ?? 0;
1969
+ avoid.set(partner, Math.max(existing, decay));
1970
+ }
1971
+ }
1972
+ }
1973
+ }
1974
+ return avoid;
1975
+ }
1976
+ /**
1977
+ * Compute interference score reduction for a card.
1978
+ * Returns: { multiplier, interfering tags, reason }
1979
+ */
1980
+ computeInterferenceEffect(cardTags, tagsToAvoid, immatureTags) {
1981
+ if (tagsToAvoid.size === 0) {
1982
+ return {
1983
+ multiplier: 1,
1984
+ interferingTags: [],
1985
+ reason: "No interference detected"
1986
+ };
1987
+ }
1988
+ let multiplier = 1;
1989
+ const interferingTags = [];
1990
+ for (const tag of cardTags) {
1991
+ const decay = tagsToAvoid.get(tag);
1992
+ if (decay !== void 0) {
1993
+ interferingTags.push(tag);
1994
+ multiplier *= 1 - decay;
1995
+ }
1996
+ }
1997
+ if (interferingTags.length === 0) {
1998
+ return {
1999
+ multiplier: 1,
2000
+ interferingTags: [],
2001
+ reason: "No interference detected"
2002
+ };
2003
+ }
2004
+ const causingTags = /* @__PURE__ */ new Set();
2005
+ for (const tag of interferingTags) {
2006
+ for (const immatureTag of immatureTags) {
2007
+ const partners = this.interferenceMap.get(immatureTag);
2008
+ if (partners?.some((p) => p.partner === tag)) {
2009
+ causingTags.add(immatureTag);
2010
+ }
2011
+ }
2012
+ }
2013
+ const reason = `Interferes with immature tags ${Array.from(causingTags).join(", ")} (tags: ${interferingTags.join(", ")}, multiplier: ${multiplier.toFixed(2)})`;
2014
+ return { multiplier, interferingTags, reason };
2015
+ }
2016
+ /**
2017
+ * CardFilter.transform implementation.
2018
+ *
2019
+ * Apply interference-aware scoring. Cards with tags that interfere with
2020
+ * immature learnings get reduced scores.
2021
+ */
2022
+ async transform(cards, context) {
2023
+ const immatureTags = await this.getImmatureTags(context);
2024
+ const tagsToAvoid = this.getTagsToAvoid(immatureTags);
2025
+ const adjusted = [];
2026
+ for (const card of cards) {
2027
+ const cardTags = card.tags ?? [];
2028
+ const { multiplier, reason } = this.computeInterferenceEffect(
2029
+ cardTags,
2030
+ tagsToAvoid,
2031
+ immatureTags
2032
+ );
2033
+ const finalScore = card.score * multiplier;
2034
+ const action = multiplier < 1 ? "penalized" : multiplier > 1 ? "boosted" : "passed";
2035
+ adjusted.push({
2036
+ ...card,
2037
+ score: finalScore,
2038
+ provenance: [
2039
+ ...card.provenance,
2040
+ {
2041
+ strategy: "interferenceMitigator",
2042
+ strategyName: this.strategyName || this.name,
2043
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-interference",
2044
+ action,
2045
+ score: finalScore,
2046
+ reason
2047
+ }
2048
+ ]
2049
+ });
2050
+ }
2051
+ return adjusted;
2052
+ }
2053
+ /**
2054
+ * Legacy getWeightedCards - now throws as filters should not be used as generators.
2055
+ *
2056
+ * Use transform() via Pipeline instead.
2057
+ */
2058
+ async getWeightedCards(_limit) {
2059
+ throw new Error(
2060
+ "InterferenceMitigatorNavigator is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
2061
+ );
2062
+ }
2063
+ };
2064
+ }
2065
+ });
2066
+
2067
+ // src/core/navigators/filters/relativePriority.ts
2068
+ var relativePriority_exports = {};
2069
+ __export(relativePriority_exports, {
2070
+ default: () => RelativePriorityNavigator
2071
+ });
2072
+ var DEFAULT_PRIORITY, DEFAULT_PRIORITY_INFLUENCE, DEFAULT_COMBINE_MODE, RelativePriorityNavigator;
2073
+ var init_relativePriority = __esm({
2074
+ "src/core/navigators/filters/relativePriority.ts"() {
2075
+ "use strict";
2076
+ init_navigators();
2077
+ DEFAULT_PRIORITY = 0.5;
2078
+ DEFAULT_PRIORITY_INFLUENCE = 0.5;
2079
+ DEFAULT_COMBINE_MODE = "max";
2080
+ RelativePriorityNavigator = class extends ContentNavigator {
2081
+ config;
2082
+ /** Human-readable name for CardFilter interface */
2083
+ name;
2084
+ constructor(user, course, strategyData) {
2085
+ super(user, course, strategyData);
2086
+ this.config = this.parseConfig(strategyData.serializedData);
2087
+ this.name = strategyData.name || "Relative Priority";
2088
+ }
2089
+ parseConfig(serializedData) {
2090
+ try {
2091
+ const parsed = JSON.parse(serializedData);
2092
+ return {
2093
+ tagPriorities: parsed.tagPriorities || {},
2094
+ defaultPriority: parsed.defaultPriority ?? DEFAULT_PRIORITY,
2095
+ combineMode: parsed.combineMode ?? DEFAULT_COMBINE_MODE,
2096
+ priorityInfluence: parsed.priorityInfluence ?? DEFAULT_PRIORITY_INFLUENCE
2097
+ };
2098
+ } catch {
2099
+ return {
2100
+ tagPriorities: {},
2101
+ defaultPriority: DEFAULT_PRIORITY,
2102
+ combineMode: DEFAULT_COMBINE_MODE,
2103
+ priorityInfluence: DEFAULT_PRIORITY_INFLUENCE
2104
+ };
2105
+ }
2106
+ }
2107
+ /**
2108
+ * Look up the priority for a tag.
2109
+ */
2110
+ getTagPriority(tagId) {
2111
+ return this.config.tagPriorities[tagId] ?? this.config.defaultPriority ?? DEFAULT_PRIORITY;
2112
+ }
2113
+ /**
2114
+ * Compute combined priority for a card based on its tags.
2115
+ */
2116
+ computeCardPriority(cardTags) {
2117
+ if (cardTags.length === 0) {
2118
+ return this.config.defaultPriority ?? DEFAULT_PRIORITY;
2119
+ }
2120
+ const priorities = cardTags.map((tag) => this.getTagPriority(tag));
2121
+ switch (this.config.combineMode) {
2122
+ case "max":
2123
+ return Math.max(...priorities);
2124
+ case "min":
2125
+ return Math.min(...priorities);
2126
+ case "average":
2127
+ return priorities.reduce((sum, p) => sum + p, 0) / priorities.length;
2128
+ default:
2129
+ return Math.max(...priorities);
2130
+ }
2131
+ }
2132
+ /**
2133
+ * Compute boost factor based on priority.
2134
+ *
2135
+ * The formula: 1 + (priority - 0.5) * priorityInfluence
2136
+ *
2137
+ * This creates a multiplier centered around 1.0:
2138
+ * - Priority 1.0 with influence 0.5 → 1.25 (25% boost)
2139
+ * - Priority 0.5 with any influence → 1.00 (neutral)
2140
+ * - Priority 0.0 with influence 0.5 → 0.75 (25% reduction)
2141
+ */
2142
+ computeBoostFactor(priority) {
2143
+ const influence = this.config.priorityInfluence ?? DEFAULT_PRIORITY_INFLUENCE;
2144
+ return 1 + (priority - 0.5) * influence;
2145
+ }
2146
+ /**
2147
+ * Build human-readable reason for priority adjustment.
2148
+ */
2149
+ buildPriorityReason(cardTags, priority, boostFactor, finalScore) {
2150
+ if (cardTags.length === 0) {
2151
+ return `No tags, neutral priority (${priority.toFixed(2)})`;
2152
+ }
2153
+ const tagList = cardTags.slice(0, 3).join(", ");
2154
+ const more = cardTags.length > 3 ? ` (+${cardTags.length - 3} more)` : "";
2155
+ if (boostFactor === 1) {
2156
+ return `Neutral priority (${priority.toFixed(2)}) for tags: ${tagList}${more}`;
2157
+ } else if (boostFactor > 1) {
2158
+ return `High-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 boost ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
2159
+ } else {
2160
+ return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
2161
+ }
2162
+ }
2163
+ /**
2164
+ * CardFilter.transform implementation.
2165
+ *
2166
+ * Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
2167
+ * cards with low-priority tags get reduced scores.
2168
+ */
2169
+ async transform(cards, _context) {
2170
+ const adjusted = await Promise.all(
2171
+ cards.map(async (card) => {
2172
+ const cardTags = card.tags ?? [];
2173
+ const priority = this.computeCardPriority(cardTags);
2174
+ const boostFactor = this.computeBoostFactor(priority);
2175
+ const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
2176
+ const action = boostFactor > 1 ? "boosted" : boostFactor < 1 ? "penalized" : "passed";
2177
+ const reason = this.buildPriorityReason(cardTags, priority, boostFactor, finalScore);
2178
+ return {
2179
+ ...card,
2180
+ score: finalScore,
2181
+ provenance: [
2182
+ ...card.provenance,
2183
+ {
2184
+ strategy: "relativePriority",
2185
+ strategyName: this.strategyName || this.name,
2186
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-priority",
2187
+ action,
2188
+ score: finalScore,
2189
+ reason
2190
+ }
2191
+ ]
2192
+ };
2193
+ })
2194
+ );
2195
+ return adjusted;
2196
+ }
2197
+ /**
2198
+ * Legacy getWeightedCards - now throws as filters should not be used as generators.
2199
+ *
2200
+ * Use transform() via Pipeline instead.
2201
+ */
2202
+ async getWeightedCards(_limit) {
2203
+ throw new Error(
2204
+ "RelativePriorityNavigator is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
2205
+ );
2206
+ }
2207
+ };
2208
+ }
2209
+ });
2210
+
2211
+ // src/core/navigators/filters/types.ts
2212
+ var types_exports2 = {};
2213
+ var init_types2 = __esm({
2214
+ "src/core/navigators/filters/types.ts"() {
2215
+ "use strict";
2216
+ }
2217
+ });
2218
+
2219
+ // src/core/navigators/filters/userGoalStub.ts
2220
+ var userGoalStub_exports = {};
2221
+ __export(userGoalStub_exports, {
2222
+ USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
2223
+ });
2224
+ var USER_GOAL_NAVIGATOR_STUB;
2225
+ var init_userGoalStub = __esm({
2226
+ "src/core/navigators/filters/userGoalStub.ts"() {
2227
+ "use strict";
2228
+ USER_GOAL_NAVIGATOR_STUB = true;
2229
+ }
2230
+ });
2231
+
2232
+ // import("./filters/**/*") in src/core/navigators/index.ts
2233
+ var globImport_filters;
2234
+ var init_2 = __esm({
2235
+ 'import("./filters/**/*") in src/core/navigators/index.ts'() {
2236
+ globImport_filters = __glob({
2237
+ "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
2238
+ "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
2239
+ "./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
2240
+ "./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
2241
+ "./filters/inferredPreferenceStub.ts": () => Promise.resolve().then(() => (init_inferredPreferenceStub(), inferredPreferenceStub_exports)),
2242
+ "./filters/interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
2243
+ "./filters/relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
2244
+ "./filters/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
2245
+ "./filters/userGoalStub.ts": () => Promise.resolve().then(() => (init_userGoalStub(), userGoalStub_exports)),
2246
+ "./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports))
2247
+ });
2248
+ }
2249
+ });
2250
+
2251
+ // src/core/orchestration/gradient.ts
2252
+ var init_gradient = __esm({
2253
+ "src/core/orchestration/gradient.ts"() {
2254
+ "use strict";
2255
+ init_logger();
2256
+ }
2257
+ });
2258
+
2259
+ // src/core/orchestration/learning.ts
2260
+ var init_learning = __esm({
2261
+ "src/core/orchestration/learning.ts"() {
2262
+ "use strict";
2263
+ init_contentNavigationStrategy();
2264
+ init_types_legacy();
2265
+ init_logger();
2266
+ }
2267
+ });
2268
+
2269
+ // src/core/orchestration/signal.ts
2270
+ var init_signal = __esm({
2271
+ "src/core/orchestration/signal.ts"() {
2272
+ "use strict";
2273
+ }
2274
+ });
2275
+
2276
+ // src/core/orchestration/recording.ts
2277
+ var init_recording = __esm({
2278
+ "src/core/orchestration/recording.ts"() {
2279
+ "use strict";
2280
+ init_signal();
2281
+ init_types_legacy();
2282
+ init_logger();
2283
+ }
2284
+ });
2285
+
2286
+ // src/core/orchestration/index.ts
2287
+ function fnv1a(str) {
2288
+ let hash = 2166136261;
2289
+ for (let i = 0; i < str.length; i++) {
2290
+ hash ^= str.charCodeAt(i);
2291
+ hash = Math.imul(hash, 16777619);
2292
+ }
2293
+ return hash >>> 0;
2294
+ }
2295
+ function computeDeviation(userId, strategyId, salt) {
2296
+ const input = `${userId}:${strategyId}:${salt}`;
2297
+ const hash = fnv1a(input);
2298
+ const normalized = hash / 4294967296;
2299
+ return normalized * 2 - 1;
2300
+ }
2301
+ function computeSpread(confidence) {
2302
+ const clampedConfidence = Math.max(0, Math.min(1, confidence));
2303
+ return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
2304
+ }
2305
+ function computeEffectiveWeight(learnable, userId, strategyId, salt) {
2306
+ const deviation = computeDeviation(userId, strategyId, salt);
2307
+ const spread = computeSpread(learnable.confidence);
2308
+ const adjustment = deviation * spread * learnable.weight;
2309
+ const effective = learnable.weight + adjustment;
2310
+ return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
2311
+ }
2312
+ async function createOrchestrationContext(user, course) {
2313
+ let courseConfig;
2314
+ try {
2315
+ courseConfig = await course.getCourseConfig();
2316
+ } catch (e) {
2317
+ logger.error(`[Orchestration] Failed to load course config: ${e}`);
2318
+ courseConfig = {
2319
+ name: "Unknown",
2320
+ description: "",
2321
+ public: false,
2322
+ deleted: false,
2323
+ creator: "",
2324
+ admins: [],
2325
+ moderators: [],
2326
+ dataShapes: [],
2327
+ questionTypes: [],
2328
+ orchestration: { salt: "default" }
2329
+ };
2330
+ }
2331
+ const userId = user.getUsername();
2332
+ const salt = courseConfig.orchestration?.salt || "default_salt";
2333
+ return {
2334
+ user,
2335
+ course,
2336
+ userId,
2337
+ courseConfig,
2338
+ getEffectiveWeight(strategyId, learnable) {
2339
+ return computeEffectiveWeight(learnable, userId, strategyId, salt);
2340
+ },
2341
+ getDeviation(strategyId) {
2342
+ return computeDeviation(userId, strategyId, salt);
2343
+ }
2344
+ };
2345
+ }
2346
+ var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
2347
+ var init_orchestration = __esm({
2348
+ "src/core/orchestration/index.ts"() {
2349
+ "use strict";
2350
+ init_logger();
2351
+ init_gradient();
2352
+ init_learning();
2353
+ init_signal();
2354
+ init_recording();
2355
+ MIN_SPREAD = 0.1;
2356
+ MAX_SPREAD = 0.5;
2357
+ MIN_WEIGHT = 0.1;
2358
+ MAX_WEIGHT = 3;
2359
+ }
2360
+ });
2361
+
2362
+ // src/core/navigators/Pipeline.ts
2363
+ var Pipeline_exports = {};
2364
+ __export(Pipeline_exports, {
2365
+ Pipeline: () => Pipeline
2366
+ });
2367
+ function logPipelineConfig(generator, filters) {
2368
+ const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
2369
+ logger.info(
2370
+ `[Pipeline] Configuration:
2371
+ Generator: ${generator.name}
2372
+ Filters:${filterList}`
2373
+ );
2374
+ }
2375
+ function logTagHydration(cards, tagsByCard) {
2376
+ const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
2377
+ const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
2378
+ logger.debug(
2379
+ `[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
2380
+ );
2381
+ }
2382
+ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores, filterImpacts) {
2383
+ const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
2384
+ let filterSummary = "";
2385
+ if (filterImpacts.length > 0) {
2386
+ const impacts = filterImpacts.map((f) => {
2387
+ const parts = [];
2388
+ if (f.boosted > 0) parts.push(`+${f.boosted}`);
2389
+ if (f.penalized > 0) parts.push(`-${f.penalized}`);
2390
+ if (f.passed > 0) parts.push(`=${f.passed}`);
2391
+ return `${f.name}: ${parts.join("/")}`;
2392
+ });
2393
+ filterSummary = `
2394
+ Filter impact: ${impacts.join(", ")}`;
2395
+ }
2396
+ logger.info(
2397
+ `[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})` + filterSummary + `
2398
+ \u{1F4A1} Inspect: window.skuilder.pipeline`
2399
+ );
2400
+ }
2401
+ function logCardProvenance(cards, maxCards = 3) {
2402
+ const cardsToLog = cards.slice(0, maxCards);
2403
+ logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
2404
+ for (const card of cardsToLog) {
2405
+ logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
2406
+ for (const entry of card.provenance) {
2407
+ const scoreChange = entry.score.toFixed(3);
2408
+ const action = entry.action.padEnd(9);
2409
+ logger.debug(
2410
+ `[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
2411
+ );
2412
+ }
2413
+ }
2414
+ }
2415
+ var import_common8, Pipeline;
2416
+ var init_Pipeline = __esm({
2417
+ "src/core/navigators/Pipeline.ts"() {
2418
+ "use strict";
2419
+ import_common8 = require("@vue-skuilder/common");
2420
+ init_navigators();
2421
+ init_logger();
2422
+ init_orchestration();
2423
+ init_PipelineDebugger();
2424
+ Pipeline = class extends ContentNavigator {
2425
+ generator;
2426
+ filters;
2427
+ /**
2428
+ * Create a new pipeline.
2429
+ *
2430
+ * @param generator - The generator (or CompositeGenerator) that produces candidates
2431
+ * @param filters - Filters to apply sequentially (order doesn't matter for multipliers)
2432
+ * @param user - User database interface
2433
+ * @param course - Course database interface
2434
+ */
2435
+ constructor(generator, filters, user, course) {
2436
+ super();
2437
+ this.generator = generator;
2438
+ this.filters = filters;
2439
+ this.user = user;
2440
+ this.course = course;
2441
+ course.getCourseConfig().then((cfg) => {
2442
+ logger.debug(`[pipeline] Crated pipeline for ${cfg.name}`);
2443
+ }).catch((e) => {
2444
+ logger.error(`[pipeline] Failed to lookup courseCfg: ${e}`);
2445
+ });
2446
+ logPipelineConfig(generator, filters);
2447
+ }
2448
+ /**
2449
+ * Get weighted cards by running generator and applying filters.
2450
+ *
2451
+ * 1. Build shared context (user ELO, etc.)
2452
+ * 2. Get candidates from generator (passing context)
2453
+ * 3. Batch hydrate tags for all candidates
2454
+ * 4. Apply each filter sequentially
2455
+ * 5. Remove zero-score cards
2456
+ * 6. Sort by score descending
2457
+ * 7. Return top N
2458
+ *
2459
+ * @param limit - Maximum number of cards to return
2460
+ * @returns Cards sorted by score descending
2461
+ */
2462
+ async getWeightedCards(limit) {
2463
+ const context = await this.buildContext();
2464
+ const overFetchMultiplier = 2 + this.filters.length * 0.5;
2465
+ const fetchLimit = Math.ceil(limit * overFetchMultiplier);
2466
+ logger.debug(
2467
+ `[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
2468
+ );
2469
+ let cards = await this.generator.getWeightedCards(fetchLimit, context);
2470
+ const generatedCount = cards.length;
2471
+ let generatorSummaries;
2472
+ if (this.generator.generators) {
2473
+ const genMap = /* @__PURE__ */ new Map();
2474
+ for (const card of cards) {
2475
+ const firstProv = card.provenance[0];
2476
+ if (firstProv) {
2477
+ const genName = firstProv.strategyName;
2478
+ if (!genMap.has(genName)) {
2479
+ genMap.set(genName, { cards: [] });
2480
+ }
2481
+ genMap.get(genName).cards.push(card);
2482
+ }
2483
+ }
2484
+ generatorSummaries = Array.from(genMap.entries()).map(([name, data]) => {
2485
+ const newCards = data.cards.filter((c) => c.provenance[0]?.reason?.includes("new card"));
2486
+ const reviewCards = data.cards.filter((c) => c.provenance[0]?.reason?.includes("review"));
2487
+ return {
2488
+ name,
2489
+ cardCount: data.cards.length,
2490
+ newCount: newCards.length,
2491
+ reviewCount: reviewCards.length,
2492
+ topScore: Math.max(...data.cards.map((c) => c.score), 0)
2493
+ };
2494
+ });
2495
+ }
2496
+ logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
2497
+ cards = await this.hydrateTags(cards);
2498
+ const allCardsBeforeFiltering = [...cards];
2499
+ const filterImpacts = [];
2500
+ for (const filter of this.filters) {
2501
+ const beforeCount = cards.length;
2502
+ const beforeScores = new Map(cards.map((c) => [c.cardId, c.score]));
2503
+ cards = await filter.transform(cards, context);
2504
+ let boosted = 0, penalized = 0, passed = 0;
2505
+ const removed = beforeCount - cards.length;
2506
+ for (const card of cards) {
2507
+ const before = beforeScores.get(card.cardId) ?? 0;
2508
+ if (card.score > before) boosted++;
2509
+ else if (card.score < before) penalized++;
2510
+ else passed++;
2511
+ }
2512
+ filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
2513
+ logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
2514
+ }
2515
+ cards = cards.filter((c) => c.score > 0);
2516
+ cards.sort((a, b) => b.score - a.score);
2517
+ const result = cards.slice(0, limit);
2518
+ const topScores = result.slice(0, 3).map((c) => c.score);
2519
+ logExecutionSummary(
2520
+ this.generator.name,
2521
+ generatedCount,
2522
+ this.filters.length,
2523
+ result.length,
2524
+ topScores,
2525
+ filterImpacts
2526
+ );
2527
+ logCardProvenance(result, 3);
2528
+ try {
2529
+ const courseName = await this.course?.getCourseConfig().then((c) => c.name).catch(() => void 0);
2530
+ const report = buildRunReport(
2531
+ this.course?.getCourseID() || "unknown",
2532
+ courseName,
2533
+ this.generator.name,
2534
+ generatorSummaries,
2535
+ generatedCount,
2536
+ filterImpacts,
2537
+ allCardsBeforeFiltering,
2538
+ result
2539
+ );
2540
+ captureRun(report);
2541
+ } catch (e) {
2542
+ logger.debug(`[Pipeline] Failed to capture debug run: ${e}`);
2543
+ }
2544
+ return result;
2545
+ }
2546
+ /**
2547
+ * Batch hydrate tags for all cards.
2548
+ *
2549
+ * Fetches tags for all cards in a single database query and attaches them
2550
+ * to the WeightedCard objects. Filters can then use card.tags instead of
2551
+ * making individual getAppliedTags() calls.
2552
+ *
2553
+ * @param cards - Cards to hydrate
2554
+ * @returns Cards with tags populated
2555
+ */
2556
+ async hydrateTags(cards) {
2557
+ if (cards.length === 0) {
2558
+ return cards;
2559
+ }
2560
+ const cardIds = cards.map((c) => c.cardId);
2561
+ const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
2562
+ logTagHydration(cards, tagsByCard);
2563
+ return cards.map((card) => ({
2564
+ ...card,
2565
+ tags: tagsByCard.get(card.cardId) ?? []
2566
+ }));
2567
+ }
2568
+ /**
2569
+ * Build shared context for generator and filters.
2570
+ *
2571
+ * Called once per getWeightedCards() invocation.
2572
+ * Contains data that the generator and multiple filters might need.
2573
+ *
2574
+ * The context satisfies both GeneratorContext and FilterContext interfaces.
2575
+ */
2576
+ async buildContext() {
2577
+ let userElo = 1e3;
2578
+ try {
2579
+ const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
2580
+ const courseElo = (0, import_common8.toCourseElo)(courseReg.elo);
2581
+ userElo = courseElo.global.score;
2582
+ } catch (e) {
2583
+ logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
2584
+ }
2585
+ const orchestration = await createOrchestrationContext(this.user, this.course);
2586
+ return {
2587
+ user: this.user,
2588
+ course: this.course,
2589
+ userElo,
2590
+ orchestration
2591
+ };
2592
+ }
2593
+ /**
2594
+ * Get the course ID for this pipeline.
2595
+ */
2596
+ getCourseID() {
2597
+ return this.course.getCourseID();
2598
+ }
2599
+ /**
2600
+ * Get orchestration context for outcome recording.
2601
+ */
2602
+ async getOrchestrationContext() {
2603
+ return createOrchestrationContext(this.user, this.course);
2604
+ }
2605
+ /**
2606
+ * Get IDs of all strategies in this pipeline.
2607
+ * Used to record which strategies contributed to an outcome.
2608
+ */
2609
+ getStrategyIds() {
2610
+ const ids = [];
2611
+ const extractId = (obj) => {
2612
+ if (obj.strategyId) return obj.strategyId;
2613
+ return null;
2614
+ };
2615
+ const genId = extractId(this.generator);
2616
+ if (genId) ids.push(genId);
2617
+ if (this.generator.generators && Array.isArray(this.generator.generators)) {
2618
+ this.generator.generators.forEach((g) => {
2619
+ const subId = extractId(g);
2620
+ if (subId) ids.push(subId);
2621
+ });
2622
+ }
2623
+ for (const filter of this.filters) {
2624
+ const fId = extractId(filter);
2625
+ if (fId) ids.push(fId);
2626
+ }
2627
+ return [...new Set(ids)];
2628
+ }
2629
+ };
2630
+ }
2631
+ });
2632
+
2633
+ // src/core/navigators/defaults.ts
2634
+ var defaults_exports = {};
2635
+ __export(defaults_exports, {
2636
+ createDefaultEloStrategy: () => createDefaultEloStrategy,
2637
+ createDefaultPipeline: () => createDefaultPipeline,
2638
+ createDefaultSrsStrategy: () => createDefaultSrsStrategy
2639
+ });
2640
+ function createDefaultEloStrategy(courseId) {
2641
+ return {
2642
+ _id: "NAVIGATION_STRATEGY-ELO-default",
2643
+ docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
2644
+ name: "ELO (default)",
2645
+ description: "Default ELO-based navigation strategy for new cards",
2646
+ implementingClass: "elo" /* ELO */,
2647
+ course: courseId,
2648
+ serializedData: ""
2649
+ };
2650
+ }
2651
+ function createDefaultSrsStrategy(courseId) {
2652
+ return {
2653
+ _id: "NAVIGATION_STRATEGY-SRS-default",
2654
+ docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
2655
+ name: "SRS (default)",
2656
+ description: "Default SRS-based navigation strategy for reviews",
2657
+ implementingClass: "srs" /* SRS */,
2658
+ course: courseId,
2659
+ serializedData: ""
2660
+ };
2661
+ }
2662
+ function createDefaultPipeline(user, course) {
2663
+ const courseId = course.getCourseID();
2664
+ const eloNavigator = new ELONavigator(user, course, createDefaultEloStrategy(courseId));
2665
+ const srsNavigator = new SRSNavigator(user, course, createDefaultSrsStrategy(courseId));
2666
+ const compositeGenerator = new CompositeGenerator([eloNavigator, srsNavigator]);
2667
+ const eloDistanceFilter = createEloDistanceFilter();
2668
+ return new Pipeline(compositeGenerator, [eloDistanceFilter], user, course);
2669
+ }
2670
+ var init_defaults = __esm({
2671
+ "src/core/navigators/defaults.ts"() {
2672
+ "use strict";
2673
+ init_navigators();
2674
+ init_Pipeline();
2675
+ init_CompositeGenerator();
2676
+ init_elo();
2677
+ init_srs();
2678
+ init_eloDistance();
2679
+ init_types_legacy();
2680
+ }
2681
+ });
2682
+
2683
+ // src/core/navigators/PipelineAssembler.ts
2684
+ var PipelineAssembler_exports = {};
2685
+ __export(PipelineAssembler_exports, {
2686
+ PipelineAssembler: () => PipelineAssembler
2687
+ });
2688
+ var PipelineAssembler;
2689
+ var init_PipelineAssembler = __esm({
2690
+ "src/core/navigators/PipelineAssembler.ts"() {
2691
+ "use strict";
2692
+ init_navigators();
2693
+ init_WeightedFilter();
2694
+ init_Pipeline();
2695
+ init_logger();
2696
+ init_CompositeGenerator();
2697
+ init_defaults();
2698
+ PipelineAssembler = class {
2699
+ /**
2700
+ * Assembles a navigation pipeline from strategy documents.
2701
+ *
2702
+ * 1. Separates into generators and filters by role
2703
+ * 2. Validates at least one generator exists (or creates default ELO)
2704
+ * 3. Instantiates generators - wraps multiple in CompositeGenerator
2705
+ * 4. Instantiates filters
2706
+ * 5. Returns Pipeline(generator, filters)
2707
+ *
2708
+ * @param input - Strategy documents plus user/course interfaces
2709
+ * @returns Assembled pipeline and any warnings
2710
+ */
2711
+ async assemble(input) {
2712
+ const { strategies, user, course } = input;
2713
+ const warnings = [];
2714
+ if (strategies.length === 0) {
2715
+ return {
2716
+ pipeline: null,
2717
+ generatorStrategies: [],
2718
+ filterStrategies: [],
2719
+ warnings
2720
+ };
2721
+ }
2722
+ const generatorStrategies = [];
2723
+ const filterStrategies = [];
2724
+ for (const s of strategies) {
2725
+ if (isGenerator(s.implementingClass)) {
2726
+ generatorStrategies.push(s);
2727
+ } else if (isFilter(s.implementingClass)) {
2728
+ filterStrategies.push(s);
2729
+ } else {
2730
+ warnings.push(`Unknown strategy type '${s.implementingClass}', skipping: ${s.name}`);
2731
+ }
2732
+ }
2733
+ if (generatorStrategies.length === 0) {
2734
+ if (filterStrategies.length > 0) {
2735
+ logger.debug(
2736
+ "[PipelineAssembler] No generator found, using default ELO and SRS with configured filters"
2737
+ );
2738
+ const courseId = course.getCourseID();
2739
+ generatorStrategies.push(createDefaultEloStrategy(courseId));
2740
+ generatorStrategies.push(createDefaultSrsStrategy(courseId));
2741
+ } else {
2742
+ warnings.push("No generator strategy found");
2743
+ return {
2744
+ pipeline: null,
2745
+ generatorStrategies: [],
2746
+ filterStrategies: [],
2747
+ warnings
2748
+ };
2749
+ }
2750
+ }
2751
+ let generator;
2752
+ if (generatorStrategies.length === 1) {
2753
+ const nav = await ContentNavigator.create(user, course, generatorStrategies[0]);
2754
+ generator = nav;
2755
+ logger.debug(`[PipelineAssembler] Using single generator: ${generatorStrategies[0].name}`);
2756
+ } else {
2757
+ logger.debug(
2758
+ `[PipelineAssembler] Using CompositeGenerator for ${generatorStrategies.length} generators: ${generatorStrategies.map((g) => g.name).join(", ")}`
2759
+ );
2760
+ generator = await CompositeGenerator.fromStrategies(user, course, generatorStrategies);
2761
+ }
2762
+ const filters = [];
2763
+ const sortedFilterStrategies = [...filterStrategies].sort(
2764
+ (a, b) => a.name.localeCompare(b.name)
2765
+ );
2766
+ for (const filterStrategy of sortedFilterStrategies) {
2767
+ try {
2768
+ const nav = await ContentNavigator.create(user, course, filterStrategy);
2769
+ if ("transform" in nav && typeof nav.transform === "function") {
2770
+ let filter = nav;
2771
+ if (filterStrategy.learnable) {
2772
+ filter = new WeightedFilter(
2773
+ filter,
2774
+ filterStrategy.learnable,
2775
+ filterStrategy.staticWeight,
2776
+ filterStrategy._id
2777
+ );
2778
+ }
2779
+ filters.push(filter);
2780
+ logger.debug(`[PipelineAssembler] Added filter: ${filterStrategy.name}`);
2781
+ } else {
2782
+ warnings.push(
2783
+ `Filter '${filterStrategy.name}' does not implement CardFilter.transform(), skipping`
2784
+ );
2785
+ }
2786
+ } catch (e) {
2787
+ warnings.push(`Failed to instantiate filter '${filterStrategy.name}': ${e}`);
2788
+ }
2789
+ }
2790
+ const pipeline = new Pipeline(generator, filters, user, course);
2791
+ logger.debug(
2792
+ `[PipelineAssembler] Assembled pipeline with ${generatorStrategies.length} generator(s) and ${filters.length} filter(s)`
2793
+ );
2794
+ return {
2795
+ pipeline,
2796
+ generatorStrategies,
2797
+ filterStrategies: sortedFilterStrategies,
2798
+ warnings
2799
+ };
2800
+ }
2801
+ };
2802
+ }
2803
+ });
2804
+
2805
+ // import("./**/*") in src/core/navigators/index.ts
2806
+ var globImport;
2807
+ var init_3 = __esm({
2808
+ 'import("./**/*") in src/core/navigators/index.ts'() {
2809
+ globImport = __glob({
2810
+ "./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
2811
+ "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
2812
+ "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
2813
+ "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
2814
+ "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
2815
+ "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
2816
+ "./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
2817
+ "./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
2818
+ "./filters/inferredPreferenceStub.ts": () => Promise.resolve().then(() => (init_inferredPreferenceStub(), inferredPreferenceStub_exports)),
2819
+ "./filters/interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
2820
+ "./filters/relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
2821
+ "./filters/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
2822
+ "./filters/userGoalStub.ts": () => Promise.resolve().then(() => (init_userGoalStub(), userGoalStub_exports)),
2823
+ "./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
2824
+ "./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
2825
+ "./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
2826
+ "./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
2827
+ "./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
2828
+ "./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
2829
+ "./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
2830
+ });
2831
+ }
2832
+ });
2833
+
2834
+ // src/core/navigators/index.ts
2835
+ var navigators_exports = {};
2836
+ __export(navigators_exports, {
2837
+ ContentNavigator: () => ContentNavigator,
2838
+ NavigatorRole: () => NavigatorRole,
2839
+ NavigatorRoles: () => NavigatorRoles,
2840
+ Navigators: () => Navigators,
2841
+ getCardOrigin: () => getCardOrigin,
2842
+ getRegisteredNavigator: () => getRegisteredNavigator,
2843
+ getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
2844
+ hasRegisteredNavigator: () => hasRegisteredNavigator,
2845
+ initializeNavigatorRegistry: () => initializeNavigatorRegistry,
2846
+ isFilter: () => isFilter,
2847
+ isGenerator: () => isGenerator,
2848
+ mountPipelineDebugger: () => mountPipelineDebugger,
2849
+ pipelineDebugAPI: () => pipelineDebugAPI,
2850
+ registerNavigator: () => registerNavigator
2851
+ });
2852
+ function registerNavigator(implementingClass, constructor) {
2853
+ navigatorRegistry.set(implementingClass, constructor);
2854
+ logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}`);
2855
+ }
2856
+ function getRegisteredNavigator(implementingClass) {
2857
+ return navigatorRegistry.get(implementingClass);
2858
+ }
2859
+ function hasRegisteredNavigator(implementingClass) {
2860
+ return navigatorRegistry.has(implementingClass);
2861
+ }
2862
+ function getRegisteredNavigatorNames() {
2863
+ return Array.from(navigatorRegistry.keys());
2864
+ }
2865
+ async function initializeNavigatorRegistry() {
2866
+ logger.debug("[NavigatorRegistry] Initializing built-in navigators...");
2867
+ const [eloModule, srsModule] = await Promise.all([
2868
+ Promise.resolve().then(() => (init_elo(), elo_exports)),
2869
+ Promise.resolve().then(() => (init_srs(), srs_exports))
2870
+ ]);
2871
+ registerNavigator("elo", eloModule.default);
2872
+ registerNavigator("srs", srsModule.default);
2873
+ const [
2874
+ hierarchyModule,
2875
+ interferenceModule,
2876
+ relativePriorityModule,
2877
+ userTagPreferenceModule
2878
+ ] = await Promise.all([
2879
+ Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
2880
+ Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
2881
+ Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
2882
+ Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports))
2883
+ ]);
2884
+ registerNavigator("hierarchyDefinition", hierarchyModule.default);
2885
+ registerNavigator("interferenceMitigator", interferenceModule.default);
2886
+ registerNavigator("relativePriority", relativePriorityModule.default);
2887
+ registerNavigator("userTagPreference", userTagPreferenceModule.default);
2888
+ logger.debug(
2889
+ `[NavigatorRegistry] Initialized ${navigatorRegistry.size} navigators: ${getRegisteredNavigatorNames().join(", ")}`
2890
+ );
2891
+ }
2892
+ function getCardOrigin(card) {
2893
+ if (card.provenance.length === 0) {
2894
+ throw new Error("Card has no provenance - cannot determine origin");
2895
+ }
2896
+ const firstEntry = card.provenance[0];
2897
+ const reason = firstEntry.reason.toLowerCase();
2898
+ if (reason.includes("failed")) {
2899
+ return "failed";
2900
+ }
2901
+ if (reason.includes("review")) {
2902
+ return "review";
2903
+ }
2904
+ return "new";
2905
+ }
2906
+ function isGenerator(impl) {
2907
+ return NavigatorRoles[impl] === "generator" /* GENERATOR */;
2908
+ }
2909
+ function isFilter(impl) {
2910
+ return NavigatorRoles[impl] === "filter" /* FILTER */;
2911
+ }
2912
+ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigator;
2913
+ var init_navigators = __esm({
2914
+ "src/core/navigators/index.ts"() {
2915
+ "use strict";
2916
+ init_PipelineDebugger();
2917
+ init_logger();
2918
+ init_();
2919
+ init_2();
2920
+ init_3();
2921
+ navigatorRegistry = /* @__PURE__ */ new Map();
2922
+ Navigators = /* @__PURE__ */ ((Navigators2) => {
2923
+ Navigators2["ELO"] = "elo";
2924
+ Navigators2["SRS"] = "srs";
2925
+ Navigators2["HIERARCHY"] = "hierarchyDefinition";
2926
+ Navigators2["INTERFERENCE"] = "interferenceMitigator";
2927
+ Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
2928
+ Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
2929
+ return Navigators2;
2930
+ })(Navigators || {});
2931
+ NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
2932
+ NavigatorRole2["GENERATOR"] = "generator";
2933
+ NavigatorRole2["FILTER"] = "filter";
2934
+ return NavigatorRole2;
2935
+ })(NavigatorRole || {});
2936
+ NavigatorRoles = {
2937
+ ["elo" /* ELO */]: "generator" /* GENERATOR */,
2938
+ ["srs" /* SRS */]: "generator" /* GENERATOR */,
2939
+ ["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
2940
+ ["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
2941
+ ["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
2942
+ ["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
2943
+ };
2944
+ ContentNavigator = class {
2945
+ /** User interface for this navigation session */
2946
+ user;
2947
+ /** Course interface for this navigation session */
2948
+ course;
2949
+ /** Human-readable name for this strategy instance (from ContentNavigationStrategyData.name) */
2950
+ strategyName;
2951
+ /** Unique document ID for this strategy instance (from ContentNavigationStrategyData._id) */
2952
+ strategyId;
2953
+ /** Evolutionary weighting configuration */
2954
+ learnable;
2955
+ /** Whether to bypass deviation (manual/static weighting) */
2956
+ staticWeight;
2957
+ /**
2958
+ * Constructor for standard navigators.
2959
+ * Call this from subclass constructors to initialize common fields.
2960
+ *
2961
+ * Note: CompositeGenerator and Pipeline call super() without args, then set
2962
+ * user/course fields directly if needed.
2963
+ */
2964
+ constructor(user, course, strategyData) {
2965
+ this.user = user;
2966
+ this.course = course;
2967
+ if (strategyData) {
2968
+ this.strategyName = strategyData.name;
2969
+ this.strategyId = strategyData._id;
2970
+ this.learnable = strategyData.learnable;
2971
+ this.staticWeight = strategyData.staticWeight;
2972
+ }
2973
+ }
2974
+ // ============================================================================
2975
+ // STRATEGY STATE HELPERS
2976
+ // ============================================================================
2977
+ //
2978
+ // These methods allow strategies to persist their own state (user preferences,
2979
+ // learned patterns, temporal tracking) in the user database.
2980
+ //
2981
+ // ============================================================================
2982
+ /**
2983
+ * Unique key identifying this strategy for state storage.
2984
+ *
2985
+ * Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
2986
+ * Override in subclasses if multiple instances of the same strategy type
2987
+ * need separate state storage.
2988
+ */
2989
+ get strategyKey() {
2990
+ return this.constructor.name;
1286
2991
  }
1287
2992
  /**
1288
- * Get review cards scored by urgency.
2993
+ * Get this strategy's persisted state for the current course.
1289
2994
  *
1290
- * Score formula combines:
1291
- * - Relative overdueness: hoursOverdue / intervalHours
1292
- * - Interval recency: exponential decay favoring shorter intervals
2995
+ * @returns The strategy's data payload, or null if no state exists
2996
+ * @throws Error if user or course is not initialized
2997
+ */
2998
+ async getStrategyState() {
2999
+ if (!this.user || !this.course) {
3000
+ throw new Error(
3001
+ `Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
3002
+ );
3003
+ }
3004
+ return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
3005
+ }
3006
+ /**
3007
+ * Persist this strategy's state for the current course.
1293
3008
  *
1294
- * Cards not yet due are excluded (not scored as 0).
3009
+ * @param data - The strategy's data payload to store
3010
+ * @throws Error if user or course is not initialized
3011
+ */
3012
+ async putStrategyState(data) {
3013
+ if (!this.user || !this.course) {
3014
+ throw new Error(
3015
+ `Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
3016
+ );
3017
+ }
3018
+ return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
3019
+ }
3020
+ /**
3021
+ * Factory method to create navigator instances.
1295
3022
  *
1296
- * This method supports both the legacy signature (limit only) and the
1297
- * CardGenerator interface signature (limit, context).
3023
+ * First checks the navigator registry for a pre-registered constructor.
3024
+ * If not found, falls back to dynamic import (for custom navigators).
1298
3025
  *
1299
- * @param limit - Maximum number of cards to return
1300
- * @param _context - Optional GeneratorContext (currently unused, but required for interface)
3026
+ * For reliable operation in test environments, call initializeNavigatorRegistry()
3027
+ * before using this method.
3028
+ *
3029
+ * @param user - User interface
3030
+ * @param course - Course interface
3031
+ * @param strategyData - Strategy configuration document
3032
+ * @returns the runtime object used to steer a study session.
1301
3033
  */
1302
- async getWeightedCards(limit, _context) {
1303
- if (!this.user || !this.course) {
1304
- throw new Error("SRSNavigator requires user and course to be set");
3034
+ static async create(user, course, strategyData) {
3035
+ const implementingClass = strategyData.implementingClass;
3036
+ const RegisteredImpl = getRegisteredNavigator(implementingClass);
3037
+ if (RegisteredImpl) {
3038
+ logger.debug(`[ContentNavigator.create] Using registered navigator: ${implementingClass}`);
3039
+ return new RegisteredImpl(user, course, strategyData);
1305
3040
  }
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);
3041
+ logger.debug(
3042
+ `[ContentNavigator.create] Navigator not in registry, attempting dynamic import: ${implementingClass}`
3043
+ );
3044
+ let NavigatorImpl;
3045
+ const variations = [".ts", ".js", ""];
3046
+ for (const ext of variations) {
3047
+ try {
3048
+ const module2 = await globImport_generators(`./generators/${implementingClass}${ext}`);
3049
+ NavigatorImpl = module2.default;
3050
+ if (NavigatorImpl) break;
3051
+ } catch (e) {
3052
+ logger.debug(`Failed to load generator ${implementingClass}${ext}:`, e);
3053
+ }
3054
+ try {
3055
+ const module2 = await globImport_filters(`./filters/${implementingClass}${ext}`);
3056
+ NavigatorImpl = module2.default;
3057
+ if (NavigatorImpl) break;
3058
+ } catch (e) {
3059
+ logger.debug(`Failed to load filter ${implementingClass}${ext}:`, e);
3060
+ }
3061
+ try {
3062
+ const module2 = await globImport(`./${implementingClass}${ext}`);
3063
+ NavigatorImpl = module2.default;
3064
+ if (NavigatorImpl) break;
3065
+ } catch (e) {
3066
+ logger.debug(`Failed to load legacy ${implementingClass}${ext}:`, e);
3067
+ }
3068
+ if (NavigatorImpl) break;
3069
+ }
3070
+ if (!NavigatorImpl) {
3071
+ throw new Error(`Could not load navigator implementation for: ${implementingClass}`);
3072
+ }
3073
+ return new NavigatorImpl(user, course, strategyData);
1330
3074
  }
1331
3075
  /**
1332
- * Compute urgency score for a review card.
3076
+ * Get cards with suitability scores and provenance trails.
1333
3077
  *
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)
3078
+ * **This is the PRIMARY API for navigation strategies.**
1338
3079
  *
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
3080
+ * Returns cards ranked by suitability score (0-1). Higher scores indicate
3081
+ * better candidates for presentation. Each card includes a provenance trail
3082
+ * documenting how strategies contributed to the final score.
3083
+ *
3084
+ * ## Implementation Required
3085
+ * All navigation strategies MUST override this method. The base class does
3086
+ * not provide a default implementation.
3087
+ *
3088
+ * ## For Generators
3089
+ * Override this method to generate candidates and compute scores based on
3090
+ * your strategy's logic (e.g., ELO proximity, review urgency). Create the
3091
+ * initial provenance entry with action='generated'.
3092
+ *
3093
+ * ## For Filters
3094
+ * Filters should implement the CardFilter interface instead and be composed
3095
+ * via Pipeline. Filters do not directly implement getWeightedCards().
1343
3096
  *
1344
- * Combined: base 0.5 + weighted average of factors * 0.45
1345
- * Result range: approximately 0.5 to 0.95
3097
+ * @param limit - Maximum cards to return
3098
+ * @returns Cards sorted by score descending, with provenance trails
1346
3099
  */
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 };
3100
+ async getWeightedCards(_limit) {
3101
+ throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
1359
3102
  }
1360
3103
  };
1361
3104
  }
1362
3105
  });
1363
3106
 
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`
1398
- }
1399
- ]
1400
- };
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;
1412
- }
1413
- });
1414
-
1415
- // src/core/navigators/defaults.ts
1416
- function createDefaultEloStrategy(courseId) {
1417
- return {
1418
- _id: "NAVIGATION_STRATEGY-ELO-default",
1419
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1420
- name: "ELO (default)",
1421
- description: "Default ELO-based navigation strategy for new cards",
1422
- implementingClass: "elo" /* ELO */,
1423
- course: courseId,
1424
- serializedData: ""
1425
- };
1426
- }
1427
- function createDefaultSrsStrategy(courseId) {
1428
- return {
1429
- _id: "NAVIGATION_STRATEGY-SRS-default",
1430
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
1431
- name: "SRS (default)",
1432
- description: "Default SRS-based navigation strategy for reviews",
1433
- implementingClass: "srs" /* SRS */,
1434
- course: courseId,
1435
- serializedData: ""
1436
- };
1437
- }
1438
- function createDefaultPipeline(user, course) {
1439
- const courseId = course.getCourseID();
1440
- const eloNavigator = new ELONavigator(user, course, createDefaultEloStrategy(courseId));
1441
- const srsNavigator = new SRSNavigator(user, course, createDefaultSrsStrategy(courseId));
1442
- const compositeGenerator = new CompositeGenerator([eloNavigator, srsNavigator]);
1443
- const eloDistanceFilter = createEloDistanceFilter();
1444
- return new Pipeline(compositeGenerator, [eloDistanceFilter], user, course);
1445
- }
1446
- var init_defaults = __esm({
1447
- "src/core/navigators/defaults.ts"() {
1448
- "use strict";
1449
- init_navigators();
1450
- init_Pipeline();
1451
- init_CompositeGenerator();
1452
- init_elo();
1453
- init_srs();
1454
- init_eloDistance();
1455
- init_types_legacy();
1456
- }
1457
- });
1458
-
1459
3107
  // src/impl/couch/courseDB.ts
1460
3108
  function randIntWeightedTowardZero(n) {
1461
3109
  return Math.floor(Math.random() * Math.random() * Math.random() * n);
@@ -1574,11 +3222,11 @@ ${JSON.stringify(config)}
1574
3222
  function isSuccessRow(row) {
1575
3223
  return "doc" in row && row.doc !== null && row.doc !== void 0;
1576
3224
  }
1577
- var import_common7, CoursesDB, CourseDB;
3225
+ var import_common9, CoursesDB, CourseDB;
1578
3226
  var init_courseDB = __esm({
1579
3227
  "src/impl/couch/courseDB.ts"() {
1580
3228
  "use strict";
1581
- import_common7 = require("@vue-skuilder/common");
3229
+ import_common9 = require("@vue-skuilder/common");
1582
3230
  init_couch();
1583
3231
  init_updateQueue();
1584
3232
  init_types_legacy();
@@ -1700,14 +3348,14 @@ var init_courseDB = __esm({
1700
3348
  docs.rows.forEach((r) => {
1701
3349
  if (isSuccessRow(r)) {
1702
3350
  if (r.doc && r.doc.elo) {
1703
- ret.push((0, import_common7.toCourseElo)(r.doc.elo));
3351
+ ret.push((0, import_common9.toCourseElo)(r.doc.elo));
1704
3352
  } else {
1705
3353
  logger.warn("no elo data for card: " + r.id);
1706
- ret.push((0, import_common7.blankCourseElo)());
3354
+ ret.push((0, import_common9.blankCourseElo)());
1707
3355
  }
1708
3356
  } else {
1709
3357
  logger.warn("no elo data for card: " + JSON.stringify(r));
1710
- ret.push((0, import_common7.blankCourseElo)());
3358
+ ret.push((0, import_common9.blankCourseElo)());
1711
3359
  }
1712
3360
  });
1713
3361
  return ret;
@@ -1902,7 +3550,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
1902
3550
  async getCourseTagStubs() {
1903
3551
  return getCourseTagStubs(this.id);
1904
3552
  }
1905
- async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common7.blankCourseElo)()) {
3553
+ async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common9.blankCourseElo)()) {
1906
3554
  try {
1907
3555
  const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
1908
3556
  if (resp.ok) {
@@ -1911,19 +3559,19 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
1911
3559
  `[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
1912
3560
  );
1913
3561
  return {
1914
- status: import_common7.Status.error,
3562
+ status: import_common9.Status.error,
1915
3563
  message: `Note was added but no cards were created: ${resp.cardCreationError}`,
1916
3564
  id: resp.id
1917
3565
  };
1918
3566
  }
1919
3567
  return {
1920
- status: import_common7.Status.ok,
3568
+ status: import_common9.Status.ok,
1921
3569
  message: "",
1922
3570
  id: resp.id
1923
3571
  };
1924
3572
  } else {
1925
3573
  return {
1926
- status: import_common7.Status.error,
3574
+ status: import_common9.Status.error,
1927
3575
  message: "Unexpected error adding note"
1928
3576
  };
1929
3577
  }
@@ -1935,7 +3583,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
1935
3583
  message: ${err.message}`
1936
3584
  );
1937
3585
  return {
1938
- status: import_common7.Status.error,
3586
+ status: import_common9.Status.error,
1939
3587
  message: `Error adding note to course. ${e.reason || err.message}`
1940
3588
  };
1941
3589
  }
@@ -2063,7 +3711,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
2063
3711
  const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
2064
3712
  return c.courseID === this.id;
2065
3713
  });
2066
- targetElo = (0, import_common7.EloToNumber)(courseDoc.elo);
3714
+ targetElo = (0, import_common9.EloToNumber)(courseDoc.elo);
2067
3715
  } catch {
2068
3716
  targetElo = 1e3;
2069
3717
  }
@@ -2475,11 +4123,11 @@ var init_classroomDB2 = __esm({
2475
4123
  });
2476
4124
 
2477
4125
  // src/study/TagFilteredContentSource.ts
2478
- var import_common8, TagFilteredContentSource;
4126
+ var import_common10, TagFilteredContentSource;
2479
4127
  var init_TagFilteredContentSource = __esm({
2480
4128
  "src/study/TagFilteredContentSource.ts"() {
2481
4129
  "use strict";
2482
- import_common8 = require("@vue-skuilder/common");
4130
+ import_common10 = require("@vue-skuilder/common");
2483
4131
  init_courseDB();
2484
4132
  init_logger();
2485
4133
  TagFilteredContentSource = class {
@@ -2565,7 +4213,7 @@ var init_TagFilteredContentSource = __esm({
2565
4213
  * @returns Cards sorted by score descending (all scores = 1.0)
2566
4214
  */
2567
4215
  async getWeightedCards(limit) {
2568
- if (!(0, import_common8.hasActiveFilter)(this.filter)) {
4216
+ if (!(0, import_common10.hasActiveFilter)(this.filter)) {
2569
4217
  logger.warn("[TagFilteredContentSource] getWeightedCards called with no active filter");
2570
4218
  return [];
2571
4219
  }
@@ -2653,19 +4301,19 @@ async function getStudySource(source, user) {
2653
4301
  if (source.type === "classroom") {
2654
4302
  return await StudentClassroomDB.factory(source.id, user);
2655
4303
  } else {
2656
- if ((0, import_common9.hasActiveFilter)(source.tagFilter)) {
4304
+ if ((0, import_common11.hasActiveFilter)(source.tagFilter)) {
2657
4305
  return new TagFilteredContentSource(source.id, source.tagFilter, user);
2658
4306
  }
2659
4307
  return getDataLayer().getCourseDB(source.id);
2660
4308
  }
2661
4309
  }
2662
- var import_common9;
4310
+ var import_common11;
2663
4311
  var init_contentSource = __esm({
2664
4312
  "src/core/interfaces/contentSource.ts"() {
2665
4313
  "use strict";
2666
4314
  init_factory();
2667
4315
  init_classroomDB2();
2668
- import_common9 = require("@vue-skuilder/common");
4316
+ import_common11 = require("@vue-skuilder/common");
2669
4317
  init_TagFilteredContentSource();
2670
4318
  }
2671
4319
  });
@@ -2721,6 +4369,13 @@ var init_strategyState = __esm({
2721
4369
  }
2722
4370
  });
2723
4371
 
4372
+ // src/core/types/userOutcome.ts
4373
+ var init_userOutcome = __esm({
4374
+ "src/core/types/userOutcome.ts"() {
4375
+ "use strict";
4376
+ }
4377
+ });
4378
+
2724
4379
  // src/core/util/index.ts
2725
4380
  function getCardHistoryID(courseID, cardID) {
2726
4381
  return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
@@ -2733,17 +4388,17 @@ var init_util = __esm({
2733
4388
  });
2734
4389
 
2735
4390
  // src/core/bulkImport/cardProcessor.ts
2736
- var import_common10;
4391
+ var import_common12;
2737
4392
  var init_cardProcessor = __esm({
2738
4393
  "src/core/bulkImport/cardProcessor.ts"() {
2739
4394
  "use strict";
2740
- import_common10 = require("@vue-skuilder/common");
4395
+ import_common12 = require("@vue-skuilder/common");
2741
4396
  init_logger();
2742
4397
  }
2743
4398
  });
2744
4399
 
2745
4400
  // src/core/bulkImport/types.ts
2746
- var init_types = __esm({
4401
+ var init_types3 = __esm({
2747
4402
  "src/core/bulkImport/types.ts"() {
2748
4403
  "use strict";
2749
4404
  }
@@ -2754,7 +4409,7 @@ var init_bulkImport = __esm({
2754
4409
  "src/core/bulkImport/index.ts"() {
2755
4410
  "use strict";
2756
4411
  init_cardProcessor();
2757
- init_types();
4412
+ init_types3();
2758
4413
  }
2759
4414
  });
2760
4415
 
@@ -2766,10 +4421,12 @@ var init_core = __esm({
2766
4421
  init_types_legacy();
2767
4422
  init_user();
2768
4423
  init_strategyState();
4424
+ init_userOutcome();
2769
4425
  init_Loggable();
2770
4426
  init_util();
2771
4427
  init_navigators();
2772
4428
  init_bulkImport();
4429
+ init_orchestration();
2773
4430
  }
2774
4431
  });
2775
4432
 
@@ -2927,6 +4584,15 @@ var init_user_course_relDB = __esm({
2927
4584
  void this.user.updateCourseSettings(this._courseId, updates);
2928
4585
  }
2929
4586
  }
4587
+ async getStrategyState(strategyKey) {
4588
+ return this.user.getStrategyState(this._courseId, strategyKey);
4589
+ }
4590
+ async putStrategyState(strategyKey, data) {
4591
+ return this.user.putStrategyState(this._courseId, strategyKey, data);
4592
+ }
4593
+ async deleteStrategyState(strategyKey) {
4594
+ return this.user.deleteStrategyState(this._courseId, strategyKey);
4595
+ }
2930
4596
  async getReviewstoDate(targetDate) {
2931
4597
  const allReviews = await this.user.getPendingReviews(this._courseId);
2932
4598
  logger.debug(
@@ -3115,13 +4781,13 @@ async function dropUserFromClassroom(user, classID) {
3115
4781
  async function getUserClassrooms(user) {
3116
4782
  return getOrCreateClassroomRegistrationsDoc(user);
3117
4783
  }
3118
- var import_common11, import_moment5, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
4784
+ var import_common13, import_moment5, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
3119
4785
  var init_BaseUserDB = __esm({
3120
4786
  "src/impl/common/BaseUserDB.ts"() {
3121
4787
  "use strict";
3122
4788
  init_core();
3123
4789
  init_util();
3124
- import_common11 = require("@vue-skuilder/common");
4790
+ import_common13 = require("@vue-skuilder/common");
3125
4791
  import_moment5 = __toESM(require("moment"), 1);
3126
4792
  init_types_legacy();
3127
4793
  init_logger();
@@ -3171,7 +4837,7 @@ Currently logged-in as ${this._username}.`
3171
4837
  );
3172
4838
  }
3173
4839
  const result = await this.syncStrategy.createAccount(username, password);
3174
- if (result.status === import_common11.Status.ok) {
4840
+ if (result.status === import_common13.Status.ok) {
3175
4841
  log3(`Account created successfully, updating username to ${username}`);
3176
4842
  this._username = username;
3177
4843
  try {
@@ -3213,7 +4879,7 @@ Currently logged-in as ${this._username}.`
3213
4879
  async resetUserData() {
3214
4880
  if (this.syncStrategy.canAuthenticate()) {
3215
4881
  return {
3216
- status: import_common11.Status.error,
4882
+ status: import_common13.Status.error,
3217
4883
  error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
3218
4884
  };
3219
4885
  }
@@ -3232,11 +4898,11 @@ Currently logged-in as ${this._username}.`
3232
4898
  await localDB.bulkDocs(docsToDelete);
3233
4899
  }
3234
4900
  await this.init();
3235
- return { status: import_common11.Status.ok };
4901
+ return { status: import_common13.Status.ok };
3236
4902
  } catch (error) {
3237
4903
  logger.error("Failed to reset user data:", error);
3238
4904
  return {
3239
- status: import_common11.Status.error,
4905
+ status: import_common13.Status.error,
3240
4906
  error: error instanceof Error ? error.message : "Unknown error during reset"
3241
4907
  };
3242
4908
  }
@@ -3963,6 +5629,19 @@ Currently logged-in as ${this._username}.`
3963
5629
  };
3964
5630
  await this.localDB.put(doc);
3965
5631
  }
5632
+ async putUserOutcome(record) {
5633
+ try {
5634
+ await this.localDB.put(record);
5635
+ } catch (err) {
5636
+ if (err.status === 409) {
5637
+ const existing = await this.localDB.get(record._id);
5638
+ record._rev = existing._rev;
5639
+ await this.localDB.put(record);
5640
+ } else {
5641
+ throw err;
5642
+ }
5643
+ }
5644
+ }
3966
5645
  async deleteStrategyState(courseId, strategyKey) {
3967
5646
  const docId = buildStrategyStateId(courseId, strategyKey);
3968
5647
  try {
@@ -4005,6 +5684,7 @@ var init_factory = __esm({
4005
5684
  "use strict";
4006
5685
  init_common();
4007
5686
  init_logger();
5687
+ init_navigators();
4008
5688
  NOT_SET = "NOT_SET";
4009
5689
  ENV = {
4010
5690
  COUCHDB_SERVER_PROTOCOL: NOT_SET,
@@ -4130,14 +5810,14 @@ var init_auth = __esm({
4130
5810
  });
4131
5811
 
4132
5812
  // src/impl/couch/CouchDBSyncStrategy.ts
4133
- var import_common13, log4, CouchDBSyncStrategy;
5813
+ var import_common15, log4, CouchDBSyncStrategy;
4134
5814
  var init_CouchDBSyncStrategy = __esm({
4135
5815
  "src/impl/couch/CouchDBSyncStrategy.ts"() {
4136
5816
  "use strict";
4137
5817
  init_factory();
4138
5818
  init_types_legacy();
4139
5819
  init_logger();
4140
- import_common13 = require("@vue-skuilder/common");
5820
+ import_common15 = require("@vue-skuilder/common");
4141
5821
  init_common();
4142
5822
  init_pouchdb_setup();
4143
5823
  init_couch();
@@ -4208,32 +5888,32 @@ var init_CouchDBSyncStrategy = __esm({
4208
5888
  }
4209
5889
  }
4210
5890
  return {
4211
- status: import_common13.Status.ok,
5891
+ status: import_common15.Status.ok,
4212
5892
  error: void 0
4213
5893
  };
4214
5894
  } else {
4215
5895
  return {
4216
- status: import_common13.Status.error,
5896
+ status: import_common15.Status.error,
4217
5897
  error: "Failed to log in after account creation"
4218
5898
  };
4219
5899
  }
4220
5900
  } else {
4221
5901
  logger.warn(`Signup not OK: ${JSON.stringify(signupRequest)}`);
4222
5902
  return {
4223
- status: import_common13.Status.error,
5903
+ status: import_common15.Status.error,
4224
5904
  error: "Account creation failed"
4225
5905
  };
4226
5906
  }
4227
5907
  } catch (e) {
4228
5908
  if (e.reason === "Document update conflict.") {
4229
5909
  return {
4230
- status: import_common13.Status.error,
5910
+ status: import_common15.Status.error,
4231
5911
  error: "This username is taken!"
4232
5912
  };
4233
5913
  }
4234
5914
  logger.error(`Error on signup: ${JSON.stringify(e)}`);
4235
5915
  return {
4236
- status: import_common13.Status.error,
5916
+ status: import_common15.Status.error,
4237
5917
  error: e.message || "Unknown error during account creation"
4238
5918
  };
4239
5919
  }