@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.
- package/dist/{contentSource-BP9hznNV.d.ts → contentSource-BmnmvH8C.d.ts} +268 -3
- package/dist/{contentSource-DsJadoBU.d.cts → contentSource-DfBbaLA-.d.cts} +268 -3
- package/dist/core/index.d.cts +310 -6
- package/dist/core/index.d.ts +310 -6
- package/dist/core/index.js +2606 -666
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +2564 -639
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-CHYrQ5pB.d.cts → dataLayerProvider-BeRXVMs5.d.cts} +1 -1
- package/dist/{dataLayerProvider-MDTxXq2l.d.ts → dataLayerProvider-CG9GfaAY.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +11 -3
- package/dist/impl/couch/index.d.ts +11 -3
- package/dist/impl/couch/index.js +2336 -656
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2316 -631
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +4 -4
- package/dist/impl/static/index.d.ts +4 -4
- package/dist/impl/static/index.js +2312 -632
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +2315 -630
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-Dj0SEgk3.d.ts → index-BWvO-_rJ.d.ts} +1 -1
- package/dist/{index-B_j6u5E4.d.cts → index-Ba7hYbHj.d.cts} +1 -1
- package/dist/index.d.cts +278 -20
- package/dist/index.d.ts +278 -20
- package/dist/index.js +3603 -720
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3529 -674
- package/dist/index.mjs.map +1 -1
- package/dist/{types-DQaXnuoc.d.ts → types-CJrLM1Ew.d.ts} +1 -1
- package/dist/{types-Bn0itutr.d.cts → types-W8n-B6HG.d.cts} +1 -1
- package/dist/{types-legacy-DDY4N-Uq.d.cts → types-legacy-JXDxinpU.d.cts} +5 -1
- package/dist/{types-legacy-DDY4N-Uq.d.ts → types-legacy-JXDxinpU.d.ts} +5 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/docs/brainstorm-navigation-paradigm.md +40 -34
- package/docs/future-orchestration-vision.md +216 -0
- package/docs/navigators-architecture.md +210 -9
- package/docs/todo-review-urgency-adaptation.md +205 -0
- package/docs/todo-strategy-authoring.md +8 -6
- package/package.json +3 -3
- package/src/core/index.ts +2 -0
- package/src/core/interfaces/contentSource.ts +7 -0
- package/src/core/interfaces/userDB.ts +50 -0
- package/src/core/navigators/Pipeline.ts +132 -5
- package/src/core/navigators/PipelineAssembler.ts +21 -22
- package/src/core/navigators/PipelineDebugger.ts +426 -0
- package/src/core/navigators/filters/WeightedFilter.ts +141 -0
- package/src/core/navigators/filters/types.ts +4 -0
- package/src/core/navigators/generators/CompositeGenerator.ts +82 -19
- package/src/core/navigators/generators/elo.ts +14 -1
- package/src/core/navigators/generators/srs.ts +146 -18
- package/src/core/navigators/generators/types.ts +4 -0
- package/src/core/navigators/index.ts +203 -13
- package/src/core/orchestration/gradient.ts +133 -0
- package/src/core/orchestration/index.ts +210 -0
- package/src/core/orchestration/learning.ts +250 -0
- package/src/core/orchestration/recording.ts +92 -0
- package/src/core/orchestration/signal.ts +67 -0
- package/src/core/types/contentNavigationStrategy.ts +38 -0
- package/src/core/types/learningState.ts +77 -0
- package/src/core/types/types-legacy.ts +4 -0
- package/src/core/types/userOutcome.ts +51 -0
- package/src/courseConfigRegistration.ts +107 -0
- package/src/factory.ts +6 -0
- package/src/impl/common/BaseUserDB.ts +16 -0
- package/src/impl/couch/user-course-relDB.ts +12 -0
- package/src/study/MixerDebugger.ts +555 -0
- package/src/study/SessionController.ts +159 -20
- package/src/study/SessionDebugger.ts +442 -0
- package/src/study/SourceMixer.ts +36 -17
- package/src/study/TODO-session-scheduling.md +133 -0
- package/src/study/index.ts +2 -0
- package/src/study/services/EloService.ts +79 -4
- package/src/study/services/ResponseProcessor.ts +130 -72
- package/src/study/services/SrsService.ts +9 -0
- package/tests/core/navigators/Pipeline.test.ts +2 -0
- package/tests/core/navigators/PipelineAssembler.test.ts +4 -4
- package/docs/todo-evolutionary-orchestration.md +0 -310
package/dist/impl/couch/index.js
CHANGED
|
@@ -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/
|
|
624
|
-
|
|
625
|
-
|
|
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
|
|
628
|
-
|
|
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
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
636
|
-
|
|
637
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
660
|
-
|
|
661
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
683
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
692
|
-
|
|
693
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
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
|
-
|
|
712
|
-
}
|
|
784
|
+
logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
|
|
785
|
+
},
|
|
713
786
|
/**
|
|
714
|
-
*
|
|
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
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
|
|
739
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
769
|
-
|
|
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/
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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
|
-
|
|
819
|
-
|
|
820
|
-
|
|
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
|
-
*
|
|
932
|
+
* Creates a CompositeGenerator from strategy data.
|
|
823
933
|
*
|
|
824
|
-
*
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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
|
-
|
|
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 [,
|
|
1008
|
-
const
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 `
|
|
1046
|
+
return `Weighted Avg of ${count} generators (${strategies}): scores [${scores}] \u2192 ${finalScore.toFixed(2)}`;
|
|
1047
1047
|
case "frequencyBoost" /* FREQUENCY_BOOST */: {
|
|
1048
|
-
const
|
|
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(
|
|
1060
|
-
const scores =
|
|
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
|
-
|
|
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
|
|
1068
|
-
const
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
2993
|
+
* Get this strategy's persisted state for the current course.
|
|
1289
2994
|
*
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1297
|
-
*
|
|
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
|
-
*
|
|
1300
|
-
*
|
|
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
|
|
1303
|
-
|
|
1304
|
-
|
|
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
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
*
|
|
3076
|
+
* Get cards with suitability scores and provenance trails.
|
|
1333
3077
|
*
|
|
1334
|
-
*
|
|
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
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1342
|
-
*
|
|
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
|
-
*
|
|
1345
|
-
*
|
|
3097
|
+
* @param limit - Maximum cards to return
|
|
3098
|
+
* @returns Cards sorted by score descending, with provenance trails
|
|
1346
3099
|
*/
|
|
1347
|
-
|
|
1348
|
-
|
|
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
|
|
3225
|
+
var import_common9, CoursesDB, CourseDB;
|
|
1578
3226
|
var init_courseDB = __esm({
|
|
1579
3227
|
"src/impl/couch/courseDB.ts"() {
|
|
1580
3228
|
"use strict";
|
|
1581
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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:
|
|
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:
|
|
3568
|
+
status: import_common9.Status.ok,
|
|
1921
3569
|
message: "",
|
|
1922
3570
|
id: resp.id
|
|
1923
3571
|
};
|
|
1924
3572
|
} else {
|
|
1925
3573
|
return {
|
|
1926
|
-
status:
|
|
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:
|
|
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,
|
|
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
|
|
4126
|
+
var import_common10, TagFilteredContentSource;
|
|
2479
4127
|
var init_TagFilteredContentSource = __esm({
|
|
2480
4128
|
"src/study/TagFilteredContentSource.ts"() {
|
|
2481
4129
|
"use strict";
|
|
2482
|
-
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
|
|
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
|
|
4391
|
+
var import_common12;
|
|
2737
4392
|
var init_cardProcessor = __esm({
|
|
2738
4393
|
"src/core/bulkImport/cardProcessor.ts"() {
|
|
2739
4394
|
"use strict";
|
|
2740
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 ===
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
5891
|
+
status: import_common15.Status.ok,
|
|
4212
5892
|
error: void 0
|
|
4213
5893
|
};
|
|
4214
5894
|
} else {
|
|
4215
5895
|
return {
|
|
4216
|
-
status:
|
|
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:
|
|
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:
|
|
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:
|
|
5916
|
+
status: import_common15.Status.error,
|
|
4237
5917
|
error: e.message || "Unknown error during account creation"
|
|
4238
5918
|
};
|
|
4239
5919
|
}
|