@vue-skuilder/db 0.1.24 → 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-BotbOOfX.d.ts → contentSource-BmnmvH8C.d.ts} +41 -0
- package/dist/{contentSource-C90LH-OH.d.cts → contentSource-DfBbaLA-.d.cts} +41 -0
- package/dist/core/index.d.cts +94 -4
- package/dist/core/index.d.ts +94 -4
- package/dist/core/index.js +530 -83
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +528 -83
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-DGKp4zFB.d.cts → dataLayerProvider-BeRXVMs5.d.cts} +1 -1
- package/dist/{dataLayerProvider-SBpz9jQf.d.ts → dataLayerProvider-CG9GfaAY.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +2 -2
- package/dist/impl/couch/index.d.ts +2 -2
- package/dist/impl/couch/index.js +526 -83
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +526 -83
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +2 -2
- package/dist/impl/static/index.d.ts +2 -2
- package/dist/impl/static/index.js +526 -83
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +526 -83
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +247 -14
- package/dist/index.d.ts +247 -14
- package/dist/index.js +1419 -140
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1409 -137
- package/dist/index.mjs.map +1 -1
- package/docs/navigators-architecture.md +22 -4
- package/docs/todo-review-urgency-adaptation.md +205 -0
- package/package.json +3 -3
- package/src/core/interfaces/userDB.ts +44 -0
- package/src/core/navigators/Pipeline.ts +86 -5
- package/src/core/navigators/PipelineAssembler.ts +7 -21
- package/src/core/navigators/PipelineDebugger.ts +426 -0
- package/src/core/navigators/generators/CompositeGenerator.ts +21 -0
- package/src/core/navigators/generators/elo.ts +14 -1
- package/src/core/navigators/generators/srs.ts +146 -18
- package/src/core/navigators/index.ts +9 -0
- package/src/impl/couch/user-course-relDB.ts +12 -0
- package/src/study/MixerDebugger.ts +555 -0
- package/src/study/SessionController.ts +95 -19
- 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/PipelineAssembler.test.ts +4 -4
package/dist/index.js
CHANGED
|
@@ -513,6 +513,15 @@ var init_user_course_relDB = __esm({
|
|
|
513
513
|
void this.user.updateCourseSettings(this._courseId, updates);
|
|
514
514
|
}
|
|
515
515
|
}
|
|
516
|
+
async getStrategyState(strategyKey) {
|
|
517
|
+
return this.user.getStrategyState(this._courseId, strategyKey);
|
|
518
|
+
}
|
|
519
|
+
async putStrategyState(strategyKey, data) {
|
|
520
|
+
return this.user.putStrategyState(this._courseId, strategyKey, data);
|
|
521
|
+
}
|
|
522
|
+
async deleteStrategyState(strategyKey) {
|
|
523
|
+
return this.user.deleteStrategyState(this._courseId, strategyKey);
|
|
524
|
+
}
|
|
516
525
|
async getReviewstoDate(targetDate) {
|
|
517
526
|
const allReviews = await this.user.getPendingReviews(this._courseId);
|
|
518
527
|
logger.debug(
|
|
@@ -863,6 +872,271 @@ var init_courseLookupDB = __esm({
|
|
|
863
872
|
}
|
|
864
873
|
});
|
|
865
874
|
|
|
875
|
+
// src/core/navigators/PipelineDebugger.ts
|
|
876
|
+
var PipelineDebugger_exports = {};
|
|
877
|
+
__export(PipelineDebugger_exports, {
|
|
878
|
+
buildRunReport: () => buildRunReport,
|
|
879
|
+
captureRun: () => captureRun,
|
|
880
|
+
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
881
|
+
pipelineDebugAPI: () => pipelineDebugAPI
|
|
882
|
+
});
|
|
883
|
+
function getOrigin(card) {
|
|
884
|
+
const firstEntry = card.provenance[0];
|
|
885
|
+
if (!firstEntry) return "unknown";
|
|
886
|
+
const reason = firstEntry.reason?.toLowerCase() || "";
|
|
887
|
+
if (reason.includes("new card")) return "new";
|
|
888
|
+
if (reason.includes("review")) return "review";
|
|
889
|
+
return "unknown";
|
|
890
|
+
}
|
|
891
|
+
function captureRun(report) {
|
|
892
|
+
const fullReport = {
|
|
893
|
+
...report,
|
|
894
|
+
runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
895
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
896
|
+
};
|
|
897
|
+
runHistory.unshift(fullReport);
|
|
898
|
+
if (runHistory.length > MAX_RUNS) {
|
|
899
|
+
runHistory.pop();
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards) {
|
|
903
|
+
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
904
|
+
const cards = allCards.map((card) => ({
|
|
905
|
+
cardId: card.cardId,
|
|
906
|
+
courseId: card.courseId,
|
|
907
|
+
origin: getOrigin(card),
|
|
908
|
+
finalScore: card.score,
|
|
909
|
+
provenance: card.provenance,
|
|
910
|
+
selected: selectedIds.has(card.cardId)
|
|
911
|
+
}));
|
|
912
|
+
const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
|
|
913
|
+
const newSelected = selectedCards.filter((c) => getOrigin(c) === "new").length;
|
|
914
|
+
return {
|
|
915
|
+
courseId,
|
|
916
|
+
courseName,
|
|
917
|
+
generatorName,
|
|
918
|
+
generators,
|
|
919
|
+
generatedCount,
|
|
920
|
+
filters,
|
|
921
|
+
finalCount: selectedCards.length,
|
|
922
|
+
reviewsSelected,
|
|
923
|
+
newSelected,
|
|
924
|
+
cards
|
|
925
|
+
};
|
|
926
|
+
}
|
|
927
|
+
function formatProvenance(provenance) {
|
|
928
|
+
return provenance.map((p) => {
|
|
929
|
+
const actionSymbol = p.action === "generated" ? "\u{1F3B2}" : p.action === "boosted" ? "\u2B06\uFE0F" : p.action === "penalized" ? "\u2B07\uFE0F" : "\u27A1\uFE0F";
|
|
930
|
+
return ` ${actionSymbol} ${p.strategyName}: ${p.score.toFixed(3)} - ${p.reason}`;
|
|
931
|
+
}).join("\n");
|
|
932
|
+
}
|
|
933
|
+
function printRunSummary(run) {
|
|
934
|
+
console.group(`\u{1F50D} Pipeline Run: ${run.courseId} (${run.courseName || "unnamed"})`);
|
|
935
|
+
logger.info(`Run ID: ${run.runId}`);
|
|
936
|
+
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
937
|
+
logger.info(`Generator: ${run.generatorName} \u2192 ${run.generatedCount} candidates`);
|
|
938
|
+
if (run.generators && run.generators.length > 0) {
|
|
939
|
+
console.group("Generator breakdown:");
|
|
940
|
+
for (const g of run.generators) {
|
|
941
|
+
logger.info(
|
|
942
|
+
` ${g.name}: ${g.cardCount} cards (${g.newCount} new, ${g.reviewCount} reviews, top: ${g.topScore.toFixed(2)})`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
console.groupEnd();
|
|
946
|
+
}
|
|
947
|
+
if (run.filters.length > 0) {
|
|
948
|
+
console.group("Filter impact:");
|
|
949
|
+
for (const f of run.filters) {
|
|
950
|
+
logger.info(` ${f.name}: \u2191${f.boosted} \u2193${f.penalized} =${f.passed} \u2715${f.removed}`);
|
|
951
|
+
}
|
|
952
|
+
console.groupEnd();
|
|
953
|
+
}
|
|
954
|
+
logger.info(
|
|
955
|
+
`Result: ${run.finalCount} cards selected (${run.newSelected} new, ${run.reviewsSelected} reviews)`
|
|
956
|
+
);
|
|
957
|
+
console.groupEnd();
|
|
958
|
+
}
|
|
959
|
+
function mountPipelineDebugger() {
|
|
960
|
+
if (typeof window === "undefined") return;
|
|
961
|
+
const win = window;
|
|
962
|
+
win.skuilder = win.skuilder || {};
|
|
963
|
+
win.skuilder.pipeline = pipelineDebugAPI;
|
|
964
|
+
}
|
|
965
|
+
var MAX_RUNS, runHistory, pipelineDebugAPI;
|
|
966
|
+
var init_PipelineDebugger = __esm({
|
|
967
|
+
"src/core/navigators/PipelineDebugger.ts"() {
|
|
968
|
+
"use strict";
|
|
969
|
+
init_logger();
|
|
970
|
+
MAX_RUNS = 10;
|
|
971
|
+
runHistory = [];
|
|
972
|
+
pipelineDebugAPI = {
|
|
973
|
+
/**
|
|
974
|
+
* Get raw run history for programmatic access.
|
|
975
|
+
*/
|
|
976
|
+
get runs() {
|
|
977
|
+
return [...runHistory];
|
|
978
|
+
},
|
|
979
|
+
/**
|
|
980
|
+
* Show summary of a specific pipeline run.
|
|
981
|
+
*/
|
|
982
|
+
showRun(idOrIndex = 0) {
|
|
983
|
+
if (runHistory.length === 0) {
|
|
984
|
+
logger.info("[Pipeline Debug] No runs captured yet.");
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
let run;
|
|
988
|
+
if (typeof idOrIndex === "number") {
|
|
989
|
+
run = runHistory[idOrIndex];
|
|
990
|
+
if (!run) {
|
|
991
|
+
logger.info(
|
|
992
|
+
`[Pipeline Debug] No run found at index ${idOrIndex}. History length: ${runHistory.length}`
|
|
993
|
+
);
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
} else {
|
|
997
|
+
run = runHistory.find((r) => r.runId.endsWith(idOrIndex));
|
|
998
|
+
if (!run) {
|
|
999
|
+
logger.info(`[Pipeline Debug] No run found matching ID '${idOrIndex}'.`);
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
printRunSummary(run);
|
|
1004
|
+
},
|
|
1005
|
+
/**
|
|
1006
|
+
* Show summary of the last pipeline run.
|
|
1007
|
+
*/
|
|
1008
|
+
showLastRun() {
|
|
1009
|
+
this.showRun(0);
|
|
1010
|
+
},
|
|
1011
|
+
/**
|
|
1012
|
+
* Show detailed provenance for a specific card.
|
|
1013
|
+
*/
|
|
1014
|
+
showCard(cardId) {
|
|
1015
|
+
for (const run of runHistory) {
|
|
1016
|
+
const card = run.cards.find((c) => c.cardId === cardId);
|
|
1017
|
+
if (card) {
|
|
1018
|
+
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
1019
|
+
logger.info(`Course: ${card.courseId}`);
|
|
1020
|
+
logger.info(`Origin: ${card.origin}`);
|
|
1021
|
+
logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
|
|
1022
|
+
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
1023
|
+
logger.info("Provenance:");
|
|
1024
|
+
logger.info(formatProvenance(card.provenance));
|
|
1025
|
+
console.groupEnd();
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
|
|
1030
|
+
},
|
|
1031
|
+
/**
|
|
1032
|
+
* Explain why reviews may or may not have been selected.
|
|
1033
|
+
*/
|
|
1034
|
+
explainReviews() {
|
|
1035
|
+
if (runHistory.length === 0) {
|
|
1036
|
+
logger.info("[Pipeline Debug] No runs captured yet.");
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
console.group("\u{1F4CB} Review Selection Analysis");
|
|
1040
|
+
for (const run of runHistory) {
|
|
1041
|
+
console.group(`Run: ${run.courseId} @ ${run.timestamp.toLocaleTimeString()}`);
|
|
1042
|
+
const allReviews = run.cards.filter((c) => c.origin === "review");
|
|
1043
|
+
const selectedReviews = allReviews.filter((c) => c.selected);
|
|
1044
|
+
if (allReviews.length === 0) {
|
|
1045
|
+
logger.info("\u274C No reviews were generated. Check SRS logs for why.");
|
|
1046
|
+
} else if (selectedReviews.length === 0) {
|
|
1047
|
+
logger.info(`\u26A0\uFE0F ${allReviews.length} reviews generated but none selected.`);
|
|
1048
|
+
logger.info("Possible reasons:");
|
|
1049
|
+
const topNewScore = Math.max(
|
|
1050
|
+
...run.cards.filter((c) => c.origin === "new" && c.selected).map((c) => c.finalScore),
|
|
1051
|
+
0
|
|
1052
|
+
);
|
|
1053
|
+
const topReviewScore = Math.max(...allReviews.map((c) => c.finalScore), 0);
|
|
1054
|
+
if (topReviewScore < topNewScore) {
|
|
1055
|
+
logger.info(
|
|
1056
|
+
` - New cards scored higher (top new: ${topNewScore.toFixed(2)}, top review: ${topReviewScore.toFixed(2)})`
|
|
1057
|
+
);
|
|
1058
|
+
}
|
|
1059
|
+
const topReview = allReviews.sort((a, b) => b.finalScore - a.finalScore)[0];
|
|
1060
|
+
if (topReview) {
|
|
1061
|
+
logger.info(` - Top review score: ${topReview.finalScore.toFixed(3)}`);
|
|
1062
|
+
logger.info(" - Its provenance:");
|
|
1063
|
+
logger.info(formatProvenance(topReview.provenance));
|
|
1064
|
+
}
|
|
1065
|
+
} else {
|
|
1066
|
+
logger.info(`\u2705 ${selectedReviews.length}/${allReviews.length} reviews selected.`);
|
|
1067
|
+
logger.info("Top selected review:");
|
|
1068
|
+
const topSelected = selectedReviews.sort((a, b) => b.finalScore - a.finalScore)[0];
|
|
1069
|
+
logger.info(formatProvenance(topSelected.provenance));
|
|
1070
|
+
}
|
|
1071
|
+
console.groupEnd();
|
|
1072
|
+
}
|
|
1073
|
+
console.groupEnd();
|
|
1074
|
+
},
|
|
1075
|
+
/**
|
|
1076
|
+
* Show all runs in compact format.
|
|
1077
|
+
*/
|
|
1078
|
+
listRuns() {
|
|
1079
|
+
if (runHistory.length === 0) {
|
|
1080
|
+
logger.info("[Pipeline Debug] No runs captured yet.");
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
console.table(
|
|
1084
|
+
runHistory.map((r) => ({
|
|
1085
|
+
id: r.runId.slice(-8),
|
|
1086
|
+
time: r.timestamp.toLocaleTimeString(),
|
|
1087
|
+
course: r.courseName || r.courseId.slice(0, 8),
|
|
1088
|
+
generated: r.generatedCount,
|
|
1089
|
+
selected: r.finalCount,
|
|
1090
|
+
new: r.newSelected,
|
|
1091
|
+
reviews: r.reviewsSelected
|
|
1092
|
+
}))
|
|
1093
|
+
);
|
|
1094
|
+
},
|
|
1095
|
+
/**
|
|
1096
|
+
* Export run history as JSON for bug reports.
|
|
1097
|
+
*/
|
|
1098
|
+
export() {
|
|
1099
|
+
const json = JSON.stringify(runHistory, null, 2);
|
|
1100
|
+
logger.info("[Pipeline Debug] Run history exported. Copy the returned string or use:");
|
|
1101
|
+
logger.info(" copy(window.skuilder.pipeline.export())");
|
|
1102
|
+
return json;
|
|
1103
|
+
},
|
|
1104
|
+
/**
|
|
1105
|
+
* Clear run history.
|
|
1106
|
+
*/
|
|
1107
|
+
clear() {
|
|
1108
|
+
runHistory.length = 0;
|
|
1109
|
+
logger.info("[Pipeline Debug] Run history cleared.");
|
|
1110
|
+
},
|
|
1111
|
+
/**
|
|
1112
|
+
* Show help.
|
|
1113
|
+
*/
|
|
1114
|
+
help() {
|
|
1115
|
+
logger.info(`
|
|
1116
|
+
\u{1F527} Pipeline Debug API
|
|
1117
|
+
|
|
1118
|
+
Commands:
|
|
1119
|
+
.showLastRun() Show summary of most recent pipeline run
|
|
1120
|
+
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
1121
|
+
.showCard(cardId) Show provenance trail for a specific card
|
|
1122
|
+
.explainReviews() Analyze why reviews were/weren't selected
|
|
1123
|
+
.listRuns() List all captured runs in table format
|
|
1124
|
+
.export() Export run history as JSON for bug reports
|
|
1125
|
+
.clear() Clear run history
|
|
1126
|
+
.runs Access raw run history array
|
|
1127
|
+
.help() Show this help message
|
|
1128
|
+
|
|
1129
|
+
Example:
|
|
1130
|
+
window.skuilder.pipeline.showLastRun()
|
|
1131
|
+
window.skuilder.pipeline.showRun(1)
|
|
1132
|
+
window.skuilder.pipeline.showCard('abc123')
|
|
1133
|
+
`);
|
|
1134
|
+
}
|
|
1135
|
+
};
|
|
1136
|
+
mountPipelineDebugger();
|
|
1137
|
+
}
|
|
1138
|
+
});
|
|
1139
|
+
|
|
866
1140
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
867
1141
|
var CompositeGenerator_exports = {};
|
|
868
1142
|
__export(CompositeGenerator_exports, {
|
|
@@ -931,6 +1205,24 @@ var init_CompositeGenerator = __esm({
|
|
|
931
1205
|
const results = await Promise.all(
|
|
932
1206
|
this.generators.map((g) => g.getWeightedCards(limit, context))
|
|
933
1207
|
);
|
|
1208
|
+
const generatorSummaries = [];
|
|
1209
|
+
results.forEach((cards, index) => {
|
|
1210
|
+
const gen = this.generators[index];
|
|
1211
|
+
const genName = gen.name || `Generator ${index}`;
|
|
1212
|
+
const newCards = cards.filter((c) => c.provenance[0]?.reason?.includes("new card"));
|
|
1213
|
+
const reviewCards = cards.filter((c) => c.provenance[0]?.reason?.includes("review"));
|
|
1214
|
+
if (cards.length > 0) {
|
|
1215
|
+
const topScore = Math.max(...cards.map((c) => c.score)).toFixed(2);
|
|
1216
|
+
const parts = [];
|
|
1217
|
+
if (newCards.length > 0) parts.push(`${newCards.length} new`);
|
|
1218
|
+
if (reviewCards.length > 0) parts.push(`${reviewCards.length} reviews`);
|
|
1219
|
+
const breakdown = parts.length > 0 ? parts.join(", ") : `${cards.length} cards`;
|
|
1220
|
+
generatorSummaries.push(`${genName}: ${breakdown} (top: ${topScore})`);
|
|
1221
|
+
} else {
|
|
1222
|
+
generatorSummaries.push(`${genName}: 0 cards`);
|
|
1223
|
+
}
|
|
1224
|
+
});
|
|
1225
|
+
logger.info(`[Composite] Generator breakdown: ${generatorSummaries.join(" | ")}`);
|
|
934
1226
|
const byCardId = /* @__PURE__ */ new Map();
|
|
935
1227
|
results.forEach((cards, index) => {
|
|
936
1228
|
const gen = this.generators[index];
|
|
@@ -1048,6 +1340,7 @@ var init_elo = __esm({
|
|
|
1048
1340
|
"use strict";
|
|
1049
1341
|
init_navigators();
|
|
1050
1342
|
import_common5 = require("@vue-skuilder/common");
|
|
1343
|
+
init_logger();
|
|
1051
1344
|
ELONavigator = class extends ContentNavigator {
|
|
1052
1345
|
/** Human-readable name for CardGenerator interface */
|
|
1053
1346
|
name;
|
|
@@ -1107,7 +1400,16 @@ var init_elo = __esm({
|
|
|
1107
1400
|
};
|
|
1108
1401
|
});
|
|
1109
1402
|
scored.sort((a, b) => b.score - a.score);
|
|
1110
|
-
|
|
1403
|
+
const result = scored.slice(0, limit);
|
|
1404
|
+
if (result.length > 0) {
|
|
1405
|
+
const topScores = result.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ");
|
|
1406
|
+
logger.info(
|
|
1407
|
+
`[ELO] Course ${this.course.getCourseID()}: ${result.length} new cards (top scores: ${topScores})`
|
|
1408
|
+
);
|
|
1409
|
+
} else {
|
|
1410
|
+
logger.info(`[ELO] Course ${this.course.getCourseID()}: No new cards available`);
|
|
1411
|
+
}
|
|
1412
|
+
return result;
|
|
1111
1413
|
}
|
|
1112
1414
|
};
|
|
1113
1415
|
}
|
|
@@ -1126,19 +1428,37 @@ var srs_exports = {};
|
|
|
1126
1428
|
__export(srs_exports, {
|
|
1127
1429
|
default: () => SRSNavigator
|
|
1128
1430
|
});
|
|
1129
|
-
var import_moment3, SRSNavigator;
|
|
1431
|
+
var import_moment3, DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_PRESSURE, SRSNavigator;
|
|
1130
1432
|
var init_srs = __esm({
|
|
1131
1433
|
"src/core/navigators/generators/srs.ts"() {
|
|
1132
1434
|
"use strict";
|
|
1133
1435
|
import_moment3 = __toESM(require("moment"), 1);
|
|
1134
1436
|
init_navigators();
|
|
1135
1437
|
init_logger();
|
|
1438
|
+
DEFAULT_HEALTHY_BACKLOG = 20;
|
|
1439
|
+
MAX_BACKLOG_PRESSURE = 0.5;
|
|
1136
1440
|
SRSNavigator = class extends ContentNavigator {
|
|
1137
1441
|
/** Human-readable name for CardGenerator interface */
|
|
1138
1442
|
name;
|
|
1443
|
+
/** Healthy backlog threshold - when exceeded, backlog pressure kicks in */
|
|
1444
|
+
healthyBacklog;
|
|
1139
1445
|
constructor(user, course, strategyData) {
|
|
1140
1446
|
super(user, course, strategyData);
|
|
1141
1447
|
this.name = strategyData?.name || "SRS";
|
|
1448
|
+
const config = this.parseConfig(strategyData?.serializedData);
|
|
1449
|
+
this.healthyBacklog = config.healthyBacklog ?? DEFAULT_HEALTHY_BACKLOG;
|
|
1450
|
+
}
|
|
1451
|
+
/**
|
|
1452
|
+
* Parse configuration from serialized JSON.
|
|
1453
|
+
*/
|
|
1454
|
+
parseConfig(serializedData) {
|
|
1455
|
+
if (!serializedData) return {};
|
|
1456
|
+
try {
|
|
1457
|
+
return JSON.parse(serializedData);
|
|
1458
|
+
} catch {
|
|
1459
|
+
logger.warn("[SRS] Failed to parse strategy config, using defaults");
|
|
1460
|
+
return {};
|
|
1461
|
+
}
|
|
1142
1462
|
}
|
|
1143
1463
|
/**
|
|
1144
1464
|
* Get review cards scored by urgency.
|
|
@@ -1146,6 +1466,7 @@ var init_srs = __esm({
|
|
|
1146
1466
|
* Score formula combines:
|
|
1147
1467
|
* - Relative overdueness: hoursOverdue / intervalHours
|
|
1148
1468
|
* - Interval recency: exponential decay favoring shorter intervals
|
|
1469
|
+
* - Backlog pressure: boost when due reviews exceed healthy threshold
|
|
1149
1470
|
*
|
|
1150
1471
|
* Cards not yet due are excluded (not scored as 0).
|
|
1151
1472
|
*
|
|
@@ -1159,11 +1480,32 @@ var init_srs = __esm({
|
|
|
1159
1480
|
if (!this.user || !this.course) {
|
|
1160
1481
|
throw new Error("SRSNavigator requires user and course to be set");
|
|
1161
1482
|
}
|
|
1162
|
-
const
|
|
1483
|
+
const courseId = this.course.getCourseID();
|
|
1484
|
+
const reviews = await this.user.getPendingReviews(courseId);
|
|
1163
1485
|
const now = import_moment3.default.utc();
|
|
1164
1486
|
const dueReviews = reviews.filter((r) => now.isAfter(import_moment3.default.utc(r.reviewTime)));
|
|
1487
|
+
const backlogPressure = this.computeBacklogPressure(dueReviews.length);
|
|
1488
|
+
if (dueReviews.length > 0) {
|
|
1489
|
+
const pressureNote = backlogPressure > 0 ? ` [backlog pressure: +${backlogPressure.toFixed(2)}]` : ` [healthy backlog]`;
|
|
1490
|
+
logger.info(
|
|
1491
|
+
`[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
|
|
1492
|
+
);
|
|
1493
|
+
} else if (reviews.length > 0) {
|
|
1494
|
+
const sortedByDue = [...reviews].sort(
|
|
1495
|
+
(a, b) => import_moment3.default.utc(a.reviewTime).diff(import_moment3.default.utc(b.reviewTime))
|
|
1496
|
+
);
|
|
1497
|
+
const nextDue = sortedByDue[0];
|
|
1498
|
+
const nextDueTime = import_moment3.default.utc(nextDue.reviewTime);
|
|
1499
|
+
const untilDue = import_moment3.default.duration(nextDueTime.diff(now));
|
|
1500
|
+
const untilDueStr = untilDue.asHours() < 1 ? `${Math.round(untilDue.asMinutes())}m` : untilDue.asHours() < 24 ? `${Math.round(untilDue.asHours())}h` : `${Math.round(untilDue.asDays())}d`;
|
|
1501
|
+
logger.info(
|
|
1502
|
+
`[SRS] Course ${courseId}: 0 reviews due now (${reviews.length} scheduled, next in ${untilDueStr})`
|
|
1503
|
+
);
|
|
1504
|
+
} else {
|
|
1505
|
+
logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
|
|
1506
|
+
}
|
|
1165
1507
|
const scored = dueReviews.map((review) => {
|
|
1166
|
-
const { score, reason } = this.computeUrgencyScore(review, now);
|
|
1508
|
+
const { score, reason } = this.computeUrgencyScore(review, now, backlogPressure);
|
|
1167
1509
|
return {
|
|
1168
1510
|
cardId: review.cardId,
|
|
1169
1511
|
courseId: review.courseId,
|
|
@@ -1181,13 +1523,35 @@ var init_srs = __esm({
|
|
|
1181
1523
|
]
|
|
1182
1524
|
};
|
|
1183
1525
|
});
|
|
1184
|
-
logger.debug(`[srsNav] got ${scored.length} weighted cards`);
|
|
1185
1526
|
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1186
1527
|
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Compute backlog pressure based on number of due reviews.
|
|
1530
|
+
*
|
|
1531
|
+
* Backlog pressure is 0 when at or below healthy threshold,
|
|
1532
|
+
* and increases linearly above it, maxing out at MAX_BACKLOG_PRESSURE.
|
|
1533
|
+
*
|
|
1534
|
+
* Examples (with default healthyBacklog=20):
|
|
1535
|
+
* - 10 due reviews → 0.00 (healthy)
|
|
1536
|
+
* - 20 due reviews → 0.00 (at threshold)
|
|
1537
|
+
* - 40 due reviews → 0.25 (2x threshold)
|
|
1538
|
+
* - 60 due reviews → 0.50 (3x threshold, maxed)
|
|
1539
|
+
*
|
|
1540
|
+
* @param dueCount - Number of reviews currently due
|
|
1541
|
+
* @returns Backlog pressure score to add to urgency (0 to MAX_BACKLOG_PRESSURE)
|
|
1542
|
+
*/
|
|
1543
|
+
computeBacklogPressure(dueCount) {
|
|
1544
|
+
if (dueCount <= this.healthyBacklog) {
|
|
1545
|
+
return 0;
|
|
1546
|
+
}
|
|
1547
|
+
const excess = dueCount - this.healthyBacklog;
|
|
1548
|
+
const pressure = excess / this.healthyBacklog * (MAX_BACKLOG_PRESSURE / 2);
|
|
1549
|
+
return Math.min(MAX_BACKLOG_PRESSURE, pressure);
|
|
1550
|
+
}
|
|
1187
1551
|
/**
|
|
1188
1552
|
* Compute urgency score for a review card.
|
|
1189
1553
|
*
|
|
1190
|
-
*
|
|
1554
|
+
* Three factors:
|
|
1191
1555
|
* 1. Relative overdueness = hoursOverdue / intervalHours
|
|
1192
1556
|
* - 2 days overdue on 3-day interval = 0.67 (urgent)
|
|
1193
1557
|
* - 2 days overdue on 180-day interval = 0.01 (not urgent)
|
|
@@ -1197,10 +1561,19 @@ var init_srs = __esm({
|
|
|
1197
1561
|
* - 30 days (720h) → ~0.56
|
|
1198
1562
|
* - 180 days → ~0.30
|
|
1199
1563
|
*
|
|
1200
|
-
*
|
|
1201
|
-
*
|
|
1564
|
+
* 3. Backlog pressure = global boost when review backlog exceeds healthy threshold
|
|
1565
|
+
* - At healthy backlog: 0
|
|
1566
|
+
* - At 2x healthy: +0.25
|
|
1567
|
+
* - At 3x+ healthy: +0.50 (max)
|
|
1568
|
+
*
|
|
1569
|
+
* Combined: base 0.5 + (urgency factors * 0.45) + backlog pressure
|
|
1570
|
+
* Result range: 0.5 to 1.0 (uncapped to allow high-urgency reviews to compete with new cards)
|
|
1571
|
+
*
|
|
1572
|
+
* @param review - The scheduled card to score
|
|
1573
|
+
* @param now - Current time
|
|
1574
|
+
* @param backlogPressure - Pre-computed backlog pressure (0 to 0.5)
|
|
1202
1575
|
*/
|
|
1203
|
-
computeUrgencyScore(review, now) {
|
|
1576
|
+
computeUrgencyScore(review, now, backlogPressure) {
|
|
1204
1577
|
const scheduledAt = import_moment3.default.utc(review.scheduledAt);
|
|
1205
1578
|
const due = import_moment3.default.utc(review.reviewTime);
|
|
1206
1579
|
const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
|
|
@@ -1209,8 +1582,19 @@ var init_srs = __esm({
|
|
|
1209
1582
|
const recencyFactor = 0.3 + 0.7 * Math.exp(-intervalHours / 720);
|
|
1210
1583
|
const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
|
|
1211
1584
|
const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
|
|
1212
|
-
const
|
|
1213
|
-
const
|
|
1585
|
+
const baseScore = 0.5 + urgency * 0.45;
|
|
1586
|
+
const score = Math.min(1, baseScore + backlogPressure);
|
|
1587
|
+
const reasonParts = [
|
|
1588
|
+
`${Math.round(hoursOverdue)}h overdue`,
|
|
1589
|
+
`interval: ${Math.round(intervalHours)}h`,
|
|
1590
|
+
`relative: ${relativeOverdue.toFixed(2)}`,
|
|
1591
|
+
`recency: ${recencyFactor.toFixed(2)}`
|
|
1592
|
+
];
|
|
1593
|
+
if (backlogPressure > 0) {
|
|
1594
|
+
reasonParts.push(`backlog: +${backlogPressure.toFixed(2)}`);
|
|
1595
|
+
}
|
|
1596
|
+
reasonParts.push("review");
|
|
1597
|
+
const reason = reasonParts.join(", ");
|
|
1214
1598
|
return { score, reason };
|
|
1215
1599
|
}
|
|
1216
1600
|
};
|
|
@@ -2486,10 +2870,23 @@ function logTagHydration(cards, tagsByCard) {
|
|
|
2486
2870
|
`[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
|
|
2487
2871
|
);
|
|
2488
2872
|
}
|
|
2489
|
-
function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
|
|
2873
|
+
function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores, filterImpacts) {
|
|
2490
2874
|
const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
|
|
2875
|
+
let filterSummary = "";
|
|
2876
|
+
if (filterImpacts.length > 0) {
|
|
2877
|
+
const impacts = filterImpacts.map((f) => {
|
|
2878
|
+
const parts = [];
|
|
2879
|
+
if (f.boosted > 0) parts.push(`+${f.boosted}`);
|
|
2880
|
+
if (f.penalized > 0) parts.push(`-${f.penalized}`);
|
|
2881
|
+
if (f.passed > 0) parts.push(`=${f.passed}`);
|
|
2882
|
+
return `${f.name}: ${parts.join("/")}`;
|
|
2883
|
+
});
|
|
2884
|
+
filterSummary = `
|
|
2885
|
+
Filter impact: ${impacts.join(", ")}`;
|
|
2886
|
+
}
|
|
2491
2887
|
logger.info(
|
|
2492
|
-
`[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
|
|
2888
|
+
`[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})` + filterSummary + `
|
|
2889
|
+
\u{1F4A1} Inspect: window.skuilder.pipeline`
|
|
2493
2890
|
);
|
|
2494
2891
|
}
|
|
2495
2892
|
function logCardProvenance(cards, maxCards = 3) {
|
|
@@ -2514,6 +2911,7 @@ var init_Pipeline = __esm({
|
|
|
2514
2911
|
init_navigators();
|
|
2515
2912
|
init_logger();
|
|
2516
2913
|
init_orchestration();
|
|
2914
|
+
init_PipelineDebugger();
|
|
2517
2915
|
Pipeline = class extends ContentNavigator {
|
|
2518
2916
|
generator;
|
|
2519
2917
|
filters;
|
|
@@ -2561,12 +2959,49 @@ var init_Pipeline = __esm({
|
|
|
2561
2959
|
);
|
|
2562
2960
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
2563
2961
|
const generatedCount = cards.length;
|
|
2962
|
+
let generatorSummaries;
|
|
2963
|
+
if (this.generator.generators) {
|
|
2964
|
+
const genMap = /* @__PURE__ */ new Map();
|
|
2965
|
+
for (const card of cards) {
|
|
2966
|
+
const firstProv = card.provenance[0];
|
|
2967
|
+
if (firstProv) {
|
|
2968
|
+
const genName = firstProv.strategyName;
|
|
2969
|
+
if (!genMap.has(genName)) {
|
|
2970
|
+
genMap.set(genName, { cards: [] });
|
|
2971
|
+
}
|
|
2972
|
+
genMap.get(genName).cards.push(card);
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
generatorSummaries = Array.from(genMap.entries()).map(([name, data]) => {
|
|
2976
|
+
const newCards = data.cards.filter((c) => c.provenance[0]?.reason?.includes("new card"));
|
|
2977
|
+
const reviewCards = data.cards.filter((c) => c.provenance[0]?.reason?.includes("review"));
|
|
2978
|
+
return {
|
|
2979
|
+
name,
|
|
2980
|
+
cardCount: data.cards.length,
|
|
2981
|
+
newCount: newCards.length,
|
|
2982
|
+
reviewCount: reviewCards.length,
|
|
2983
|
+
topScore: Math.max(...data.cards.map((c) => c.score), 0)
|
|
2984
|
+
};
|
|
2985
|
+
});
|
|
2986
|
+
}
|
|
2564
2987
|
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
2565
2988
|
cards = await this.hydrateTags(cards);
|
|
2989
|
+
const allCardsBeforeFiltering = [...cards];
|
|
2990
|
+
const filterImpacts = [];
|
|
2566
2991
|
for (const filter of this.filters) {
|
|
2567
2992
|
const beforeCount = cards.length;
|
|
2993
|
+
const beforeScores = new Map(cards.map((c) => [c.cardId, c.score]));
|
|
2568
2994
|
cards = await filter.transform(cards, context);
|
|
2569
|
-
|
|
2995
|
+
let boosted = 0, penalized = 0, passed = 0;
|
|
2996
|
+
const removed = beforeCount - cards.length;
|
|
2997
|
+
for (const card of cards) {
|
|
2998
|
+
const before = beforeScores.get(card.cardId) ?? 0;
|
|
2999
|
+
if (card.score > before) boosted++;
|
|
3000
|
+
else if (card.score < before) penalized++;
|
|
3001
|
+
else passed++;
|
|
3002
|
+
}
|
|
3003
|
+
filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
|
|
3004
|
+
logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
|
|
2570
3005
|
}
|
|
2571
3006
|
cards = cards.filter((c) => c.score > 0);
|
|
2572
3007
|
cards.sort((a, b) => b.score - a.score);
|
|
@@ -2577,9 +3012,26 @@ var init_Pipeline = __esm({
|
|
|
2577
3012
|
generatedCount,
|
|
2578
3013
|
this.filters.length,
|
|
2579
3014
|
result.length,
|
|
2580
|
-
topScores
|
|
3015
|
+
topScores,
|
|
3016
|
+
filterImpacts
|
|
2581
3017
|
);
|
|
2582
3018
|
logCardProvenance(result, 3);
|
|
3019
|
+
try {
|
|
3020
|
+
const courseName = await this.course?.getCourseConfig().then((c) => c.name).catch(() => void 0);
|
|
3021
|
+
const report = buildRunReport(
|
|
3022
|
+
this.course?.getCourseID() || "unknown",
|
|
3023
|
+
courseName,
|
|
3024
|
+
this.generator.name,
|
|
3025
|
+
generatorSummaries,
|
|
3026
|
+
generatedCount,
|
|
3027
|
+
filterImpacts,
|
|
3028
|
+
allCardsBeforeFiltering,
|
|
3029
|
+
result
|
|
3030
|
+
);
|
|
3031
|
+
captureRun(report);
|
|
3032
|
+
} catch (e) {
|
|
3033
|
+
logger.debug(`[Pipeline] Failed to capture debug run: ${e}`);
|
|
3034
|
+
}
|
|
2583
3035
|
return result;
|
|
2584
3036
|
}
|
|
2585
3037
|
/**
|
|
@@ -2669,6 +3121,56 @@ var init_Pipeline = __esm({
|
|
|
2669
3121
|
}
|
|
2670
3122
|
});
|
|
2671
3123
|
|
|
3124
|
+
// src/core/navigators/defaults.ts
|
|
3125
|
+
var defaults_exports = {};
|
|
3126
|
+
__export(defaults_exports, {
|
|
3127
|
+
createDefaultEloStrategy: () => createDefaultEloStrategy,
|
|
3128
|
+
createDefaultPipeline: () => createDefaultPipeline,
|
|
3129
|
+
createDefaultSrsStrategy: () => createDefaultSrsStrategy
|
|
3130
|
+
});
|
|
3131
|
+
function createDefaultEloStrategy(courseId) {
|
|
3132
|
+
return {
|
|
3133
|
+
_id: "NAVIGATION_STRATEGY-ELO-default",
|
|
3134
|
+
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
3135
|
+
name: "ELO (default)",
|
|
3136
|
+
description: "Default ELO-based navigation strategy for new cards",
|
|
3137
|
+
implementingClass: "elo" /* ELO */,
|
|
3138
|
+
course: courseId,
|
|
3139
|
+
serializedData: ""
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
function createDefaultSrsStrategy(courseId) {
|
|
3143
|
+
return {
|
|
3144
|
+
_id: "NAVIGATION_STRATEGY-SRS-default",
|
|
3145
|
+
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
3146
|
+
name: "SRS (default)",
|
|
3147
|
+
description: "Default SRS-based navigation strategy for reviews",
|
|
3148
|
+
implementingClass: "srs" /* SRS */,
|
|
3149
|
+
course: courseId,
|
|
3150
|
+
serializedData: ""
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
function createDefaultPipeline(user, course) {
|
|
3154
|
+
const courseId = course.getCourseID();
|
|
3155
|
+
const eloNavigator = new ELONavigator(user, course, createDefaultEloStrategy(courseId));
|
|
3156
|
+
const srsNavigator = new SRSNavigator(user, course, createDefaultSrsStrategy(courseId));
|
|
3157
|
+
const compositeGenerator = new CompositeGenerator([eloNavigator, srsNavigator]);
|
|
3158
|
+
const eloDistanceFilter = createEloDistanceFilter();
|
|
3159
|
+
return new Pipeline(compositeGenerator, [eloDistanceFilter], user, course);
|
|
3160
|
+
}
|
|
3161
|
+
var init_defaults = __esm({
|
|
3162
|
+
"src/core/navigators/defaults.ts"() {
|
|
3163
|
+
"use strict";
|
|
3164
|
+
init_navigators();
|
|
3165
|
+
init_Pipeline();
|
|
3166
|
+
init_CompositeGenerator();
|
|
3167
|
+
init_elo();
|
|
3168
|
+
init_srs();
|
|
3169
|
+
init_eloDistance();
|
|
3170
|
+
init_types_legacy();
|
|
3171
|
+
}
|
|
3172
|
+
});
|
|
3173
|
+
|
|
2672
3174
|
// src/core/navigators/PipelineAssembler.ts
|
|
2673
3175
|
var PipelineAssembler_exports = {};
|
|
2674
3176
|
__export(PipelineAssembler_exports, {
|
|
@@ -2681,9 +3183,9 @@ var init_PipelineAssembler = __esm({
|
|
|
2681
3183
|
init_navigators();
|
|
2682
3184
|
init_WeightedFilter();
|
|
2683
3185
|
init_Pipeline();
|
|
2684
|
-
init_types_legacy();
|
|
2685
3186
|
init_logger();
|
|
2686
3187
|
init_CompositeGenerator();
|
|
3188
|
+
init_defaults();
|
|
2687
3189
|
PipelineAssembler = class {
|
|
2688
3190
|
/**
|
|
2689
3191
|
* Assembles a navigation pipeline from strategy documents.
|
|
@@ -2722,9 +3224,11 @@ var init_PipelineAssembler = __esm({
|
|
|
2722
3224
|
if (generatorStrategies.length === 0) {
|
|
2723
3225
|
if (filterStrategies.length > 0) {
|
|
2724
3226
|
logger.debug(
|
|
2725
|
-
"[PipelineAssembler] No generator found, using default ELO with configured filters"
|
|
3227
|
+
"[PipelineAssembler] No generator found, using default ELO and SRS with configured filters"
|
|
2726
3228
|
);
|
|
2727
|
-
|
|
3229
|
+
const courseId = course.getCourseID();
|
|
3230
|
+
generatorStrategies.push(createDefaultEloStrategy(courseId));
|
|
3231
|
+
generatorStrategies.push(createDefaultSrsStrategy(courseId));
|
|
2728
3232
|
} else {
|
|
2729
3233
|
warnings.push("No generator strategy found");
|
|
2730
3234
|
return {
|
|
@@ -2785,75 +3289,10 @@ var init_PipelineAssembler = __esm({
|
|
|
2785
3289
|
warnings
|
|
2786
3290
|
};
|
|
2787
3291
|
}
|
|
2788
|
-
/**
|
|
2789
|
-
* Creates a default ELO generator strategy.
|
|
2790
|
-
* Used when filters are configured but no generator is specified.
|
|
2791
|
-
*/
|
|
2792
|
-
makeDefaultEloStrategy(courseId) {
|
|
2793
|
-
return {
|
|
2794
|
-
_id: "NAVIGATION_STRATEGY-ELO-default",
|
|
2795
|
-
course: courseId,
|
|
2796
|
-
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
2797
|
-
name: "ELO (default)",
|
|
2798
|
-
description: "Default ELO-based generator",
|
|
2799
|
-
implementingClass: "elo" /* ELO */,
|
|
2800
|
-
serializedData: ""
|
|
2801
|
-
};
|
|
2802
|
-
}
|
|
2803
3292
|
};
|
|
2804
3293
|
}
|
|
2805
3294
|
});
|
|
2806
3295
|
|
|
2807
|
-
// src/core/navigators/defaults.ts
|
|
2808
|
-
var defaults_exports = {};
|
|
2809
|
-
__export(defaults_exports, {
|
|
2810
|
-
createDefaultEloStrategy: () => createDefaultEloStrategy,
|
|
2811
|
-
createDefaultPipeline: () => createDefaultPipeline,
|
|
2812
|
-
createDefaultSrsStrategy: () => createDefaultSrsStrategy
|
|
2813
|
-
});
|
|
2814
|
-
function createDefaultEloStrategy(courseId) {
|
|
2815
|
-
return {
|
|
2816
|
-
_id: "NAVIGATION_STRATEGY-ELO-default",
|
|
2817
|
-
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
2818
|
-
name: "ELO (default)",
|
|
2819
|
-
description: "Default ELO-based navigation strategy for new cards",
|
|
2820
|
-
implementingClass: "elo" /* ELO */,
|
|
2821
|
-
course: courseId,
|
|
2822
|
-
serializedData: ""
|
|
2823
|
-
};
|
|
2824
|
-
}
|
|
2825
|
-
function createDefaultSrsStrategy(courseId) {
|
|
2826
|
-
return {
|
|
2827
|
-
_id: "NAVIGATION_STRATEGY-SRS-default",
|
|
2828
|
-
docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
|
|
2829
|
-
name: "SRS (default)",
|
|
2830
|
-
description: "Default SRS-based navigation strategy for reviews",
|
|
2831
|
-
implementingClass: "srs" /* SRS */,
|
|
2832
|
-
course: courseId,
|
|
2833
|
-
serializedData: ""
|
|
2834
|
-
};
|
|
2835
|
-
}
|
|
2836
|
-
function createDefaultPipeline(user, course) {
|
|
2837
|
-
const courseId = course.getCourseID();
|
|
2838
|
-
const eloNavigator = new ELONavigator(user, course, createDefaultEloStrategy(courseId));
|
|
2839
|
-
const srsNavigator = new SRSNavigator(user, course, createDefaultSrsStrategy(courseId));
|
|
2840
|
-
const compositeGenerator = new CompositeGenerator([eloNavigator, srsNavigator]);
|
|
2841
|
-
const eloDistanceFilter = createEloDistanceFilter();
|
|
2842
|
-
return new Pipeline(compositeGenerator, [eloDistanceFilter], user, course);
|
|
2843
|
-
}
|
|
2844
|
-
var init_defaults = __esm({
|
|
2845
|
-
"src/core/navigators/defaults.ts"() {
|
|
2846
|
-
"use strict";
|
|
2847
|
-
init_navigators();
|
|
2848
|
-
init_Pipeline();
|
|
2849
|
-
init_CompositeGenerator();
|
|
2850
|
-
init_elo();
|
|
2851
|
-
init_srs();
|
|
2852
|
-
init_eloDistance();
|
|
2853
|
-
init_types_legacy();
|
|
2854
|
-
}
|
|
2855
|
-
});
|
|
2856
|
-
|
|
2857
3296
|
// import("./**/*") in src/core/navigators/index.ts
|
|
2858
3297
|
var globImport;
|
|
2859
3298
|
var init_3 = __esm({
|
|
@@ -2861,6 +3300,7 @@ var init_3 = __esm({
|
|
|
2861
3300
|
globImport = __glob({
|
|
2862
3301
|
"./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
|
|
2863
3302
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
3303
|
+
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
2864
3304
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
2865
3305
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
2866
3306
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
@@ -2896,6 +3336,8 @@ __export(navigators_exports, {
|
|
|
2896
3336
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
2897
3337
|
isFilter: () => isFilter,
|
|
2898
3338
|
isGenerator: () => isGenerator,
|
|
3339
|
+
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
3340
|
+
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
2899
3341
|
registerNavigator: () => registerNavigator
|
|
2900
3342
|
});
|
|
2901
3343
|
function registerNavigator(implementingClass, constructor) {
|
|
@@ -2962,6 +3404,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
2962
3404
|
var init_navigators = __esm({
|
|
2963
3405
|
"src/core/navigators/index.ts"() {
|
|
2964
3406
|
"use strict";
|
|
3407
|
+
init_PipelineDebugger();
|
|
2965
3408
|
init_logger();
|
|
2966
3409
|
init_();
|
|
2967
3410
|
init_2();
|
|
@@ -7206,6 +7649,7 @@ __export(index_exports, {
|
|
|
7206
7649
|
aggregateOutcomesForGradient: () => aggregateOutcomesForGradient,
|
|
7207
7650
|
areQuestionRecords: () => areQuestionRecords,
|
|
7208
7651
|
buildStrategyStateId: () => buildStrategyStateId,
|
|
7652
|
+
captureMixerRun: () => captureMixerRun,
|
|
7209
7653
|
computeDeviation: () => computeDeviation,
|
|
7210
7654
|
computeEffectiveWeight: () => computeEffectiveWeight,
|
|
7211
7655
|
computeOutcomeSignal: () => computeOutcomeSignal,
|
|
@@ -7213,6 +7657,7 @@ __export(index_exports, {
|
|
|
7213
7657
|
computeStrategyGradient: () => computeStrategyGradient,
|
|
7214
7658
|
createOrchestrationContext: () => createOrchestrationContext,
|
|
7215
7659
|
docIsDeleted: () => docIsDeleted,
|
|
7660
|
+
endSessionTracking: () => endSessionTracking,
|
|
7216
7661
|
ensureAppDataDirectory: () => ensureAppDataDirectory,
|
|
7217
7662
|
getAppDataDirectory: () => getAppDataDirectory,
|
|
7218
7663
|
getCardHistoryID: () => getCardHistoryID,
|
|
@@ -7236,9 +7681,15 @@ __export(index_exports, {
|
|
|
7236
7681
|
isQuestionTypeRegistered: () => isQuestionTypeRegistered,
|
|
7237
7682
|
isReview: () => isReview,
|
|
7238
7683
|
log: () => log,
|
|
7684
|
+
mixerDebugAPI: () => mixerDebugAPI,
|
|
7685
|
+
mountMixerDebugger: () => mountMixerDebugger,
|
|
7686
|
+
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
7687
|
+
mountSessionDebugger: () => mountSessionDebugger,
|
|
7239
7688
|
newInterval: () => newInterval,
|
|
7240
7689
|
parseCardHistoryID: () => parseCardHistoryID,
|
|
7690
|
+
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
7241
7691
|
processCustomQuestionsData: () => processCustomQuestionsData,
|
|
7692
|
+
recordCardPresentation: () => recordCardPresentation,
|
|
7242
7693
|
recordUserOutcome: () => recordUserOutcome,
|
|
7243
7694
|
registerBlanksCard: () => registerBlanksCard,
|
|
7244
7695
|
registerCustomQuestionTypes: () => registerCustomQuestionTypes,
|
|
@@ -7251,6 +7702,9 @@ __export(index_exports, {
|
|
|
7251
7702
|
removeQuestionType: () => removeQuestionType,
|
|
7252
7703
|
runPeriodUpdate: () => runPeriodUpdate,
|
|
7253
7704
|
scoreAccuracyInZone: () => scoreAccuracyInZone,
|
|
7705
|
+
sessionDebugAPI: () => sessionDebugAPI,
|
|
7706
|
+
snapshotQueues: () => snapshotQueues,
|
|
7707
|
+
startSessionTracking: () => startSessionTracking,
|
|
7254
7708
|
updateLearningState: () => updateLearningState,
|
|
7255
7709
|
updateStrategyWeight: () => updateStrategyWeight,
|
|
7256
7710
|
validateMigration: () => validateMigration,
|
|
@@ -7651,6 +8105,14 @@ var SrsService = class {
|
|
|
7651
8105
|
constructor(user) {
|
|
7652
8106
|
this.user = user;
|
|
7653
8107
|
}
|
|
8108
|
+
/**
|
|
8109
|
+
* Remove a scheduled review from the user's database.
|
|
8110
|
+
* Used to clean up orphaned reviews (e.g., card deleted from course DB).
|
|
8111
|
+
*/
|
|
8112
|
+
removeReview(reviewID) {
|
|
8113
|
+
logger.info(`[SrsService] Removing orphaned scheduled review: ${reviewID}`);
|
|
8114
|
+
void this.user.removeScheduledCardReview(reviewID);
|
|
8115
|
+
}
|
|
7654
8116
|
/**
|
|
7655
8117
|
* Calculates the next review time for a card based on its history and
|
|
7656
8118
|
* schedules it in the user's database.
|
|
@@ -7689,7 +8151,7 @@ var EloService = class {
|
|
|
7689
8151
|
* Updates both user and card ELO ratings based on user performance.
|
|
7690
8152
|
* @param userScore Score between 0-1 representing user performance
|
|
7691
8153
|
* @param course_id Course identifier
|
|
7692
|
-
* @param card_id Card identifier
|
|
8154
|
+
* @param card_id Card identifier
|
|
7693
8155
|
* @param userCourseRegDoc User's course registration document (will be mutated)
|
|
7694
8156
|
* @param currentCard Current card session record
|
|
7695
8157
|
* @param k Optional K-factor for ELO calculation
|
|
@@ -7699,7 +8161,9 @@ var EloService = class {
|
|
|
7699
8161
|
logger.warn(`k value interpretation not currently implemented`);
|
|
7700
8162
|
}
|
|
7701
8163
|
const courseDB = this.dataLayer.getCourseDB(currentCard.card.course_id);
|
|
7702
|
-
const userElo = (0, import_common22.toCourseElo)(
|
|
8164
|
+
const userElo = (0, import_common22.toCourseElo)(
|
|
8165
|
+
userCourseRegDoc.courses.find((c) => c.courseID === course_id).elo
|
|
8166
|
+
);
|
|
7703
8167
|
const cardElo = (await courseDB.getCardEloData([currentCard.card.card_id]))[0];
|
|
7704
8168
|
if (cardElo && userElo) {
|
|
7705
8169
|
const eloUpdate = (0, import_common22.adjustCourseScores)(userElo, cardElo, userScore);
|
|
@@ -7736,11 +8200,64 @@ var EloService = class {
|
|
|
7736
8200
|
}
|
|
7737
8201
|
}
|
|
7738
8202
|
}
|
|
8203
|
+
/**
|
|
8204
|
+
* Updates both user and card ELO ratings with per-tag granularity.
|
|
8205
|
+
* Tags in taggedPerformance but not on card will be created dynamically.
|
|
8206
|
+
*
|
|
8207
|
+
* @param taggedPerformance Performance object with _global and per-tag scores
|
|
8208
|
+
* @param course_id Course identifier
|
|
8209
|
+
* @param card_id Card identifier
|
|
8210
|
+
* @param userCourseRegDoc User's course registration document (will be mutated)
|
|
8211
|
+
* @param currentCard Current card session record
|
|
8212
|
+
*/
|
|
8213
|
+
async updateUserAndCardEloPerTag(taggedPerformance, course_id, card_id, userCourseRegDoc, currentCard) {
|
|
8214
|
+
const courseDB = this.dataLayer.getCourseDB(currentCard.card.course_id);
|
|
8215
|
+
const userElo = (0, import_common22.toCourseElo)(
|
|
8216
|
+
userCourseRegDoc.courses.find((c) => c.courseID === course_id).elo
|
|
8217
|
+
);
|
|
8218
|
+
const cardElo = (await courseDB.getCardEloData([currentCard.card.card_id]))[0];
|
|
8219
|
+
if (cardElo && userElo) {
|
|
8220
|
+
const eloUpdate = (0, import_common22.adjustCourseScoresPerTag)(userElo, cardElo, taggedPerformance);
|
|
8221
|
+
userCourseRegDoc.courses.find((c) => c.courseID === course_id).elo = eloUpdate.userElo;
|
|
8222
|
+
const results = await Promise.allSettled([
|
|
8223
|
+
this.user.updateUserElo(course_id, eloUpdate.userElo),
|
|
8224
|
+
courseDB.updateCardElo(card_id, eloUpdate.cardElo)
|
|
8225
|
+
]);
|
|
8226
|
+
const userEloStatus = results[0].status === "fulfilled";
|
|
8227
|
+
const cardEloStatus = results[1].status === "fulfilled";
|
|
8228
|
+
if (userEloStatus && cardEloStatus) {
|
|
8229
|
+
const user = results[0].value;
|
|
8230
|
+
const card = results[1].value;
|
|
8231
|
+
if (user.ok && card && card.ok) {
|
|
8232
|
+
const tagCount = Object.keys(taggedPerformance).length - 1;
|
|
8233
|
+
logger.info(
|
|
8234
|
+
`[EloService] Updated ELOS (per-tag, ${tagCount} tags):
|
|
8235
|
+
User: ${JSON.stringify(eloUpdate.userElo)})
|
|
8236
|
+
Card: ${JSON.stringify(eloUpdate.cardElo)})
|
|
8237
|
+
`
|
|
8238
|
+
);
|
|
8239
|
+
}
|
|
8240
|
+
} else {
|
|
8241
|
+
logger.warn(
|
|
8242
|
+
`[EloService] Partial ELO update (per-tag):
|
|
8243
|
+
User ELO update: ${userEloStatus ? "SUCCESS" : "FAILED"}
|
|
8244
|
+
Card ELO update: ${cardEloStatus ? "SUCCESS" : "FAILED"}`
|
|
8245
|
+
);
|
|
8246
|
+
if (!userEloStatus && results[0].status === "rejected") {
|
|
8247
|
+
logger.error("[EloService] User ELO update error:", results[0].reason);
|
|
8248
|
+
}
|
|
8249
|
+
if (!cardEloStatus && results[1].status === "rejected") {
|
|
8250
|
+
logger.error("[EloService] Card ELO update error:", results[1].reason);
|
|
8251
|
+
}
|
|
8252
|
+
}
|
|
8253
|
+
}
|
|
8254
|
+
}
|
|
7739
8255
|
};
|
|
7740
8256
|
|
|
7741
8257
|
// src/study/services/ResponseProcessor.ts
|
|
7742
8258
|
init_core();
|
|
7743
8259
|
init_logger();
|
|
8260
|
+
var import_common23 = require("@vue-skuilder/common");
|
|
7744
8261
|
var ResponseProcessor = class {
|
|
7745
8262
|
srsService;
|
|
7746
8263
|
eloService;
|
|
@@ -7748,6 +8265,33 @@ var ResponseProcessor = class {
|
|
|
7748
8265
|
this.srsService = srsService;
|
|
7749
8266
|
this.eloService = eloService;
|
|
7750
8267
|
}
|
|
8268
|
+
/**
|
|
8269
|
+
* Parses performance data into global score and optional per-tag scores.
|
|
8270
|
+
*
|
|
8271
|
+
* @param performance - Numeric or structured performance from QuestionRecord
|
|
8272
|
+
* @returns Parsed performance with global score and optional tag scores
|
|
8273
|
+
*/
|
|
8274
|
+
parsePerformance(performance2) {
|
|
8275
|
+
if (typeof performance2 === "number") {
|
|
8276
|
+
return {
|
|
8277
|
+
globalScore: performance2,
|
|
8278
|
+
taggedPerformance: null
|
|
8279
|
+
};
|
|
8280
|
+
}
|
|
8281
|
+
if ((0, import_common23.isTaggedPerformance)(performance2)) {
|
|
8282
|
+
return {
|
|
8283
|
+
globalScore: performance2._global,
|
|
8284
|
+
taggedPerformance: performance2
|
|
8285
|
+
};
|
|
8286
|
+
}
|
|
8287
|
+
logger.warn("[ResponseProcessor] Unexpected performance structure, using neutral score", {
|
|
8288
|
+
performance: performance2
|
|
8289
|
+
});
|
|
8290
|
+
return {
|
|
8291
|
+
globalScore: 0.5,
|
|
8292
|
+
taggedPerformance: null
|
|
8293
|
+
};
|
|
8294
|
+
}
|
|
7751
8295
|
/**
|
|
7752
8296
|
* Processes a user's response to a card, handling SRS scheduling and ELO updates.
|
|
7753
8297
|
* @param cardRecord User's response record
|
|
@@ -7808,46 +8352,60 @@ var ResponseProcessor = class {
|
|
|
7808
8352
|
processCorrectResponse(cardRecord, history, studySessionItem, courseRegistrationDoc, currentCard, courseId, cardId) {
|
|
7809
8353
|
if (cardRecord.priorAttemps === 0) {
|
|
7810
8354
|
void this.srsService.scheduleReview(history, studySessionItem);
|
|
7811
|
-
|
|
7812
|
-
|
|
7813
|
-
void this.eloService.
|
|
7814
|
-
|
|
8355
|
+
const { globalScore, taggedPerformance } = this.parsePerformance(cardRecord.performance);
|
|
8356
|
+
if (taggedPerformance) {
|
|
8357
|
+
void this.eloService.updateUserAndCardEloPerTag(
|
|
8358
|
+
taggedPerformance,
|
|
7815
8359
|
courseId,
|
|
7816
8360
|
cardId,
|
|
7817
8361
|
courseRegistrationDoc,
|
|
7818
8362
|
currentCard
|
|
7819
8363
|
);
|
|
8364
|
+
logger.info(
|
|
8365
|
+
`[ResponseProcessor] Processed correct response with per-tag ELO update (${Object.keys(taggedPerformance).length - 1} tags)`
|
|
8366
|
+
);
|
|
7820
8367
|
} else {
|
|
7821
|
-
const
|
|
7822
|
-
|
|
7823
|
-
|
|
7824
|
-
|
|
7825
|
-
|
|
7826
|
-
|
|
7827
|
-
|
|
7828
|
-
|
|
7829
|
-
|
|
8368
|
+
const userScore = 0.5 + globalScore / 2;
|
|
8369
|
+
if (history.records.length === 1) {
|
|
8370
|
+
void this.eloService.updateUserAndCardElo(
|
|
8371
|
+
userScore,
|
|
8372
|
+
courseId,
|
|
8373
|
+
cardId,
|
|
8374
|
+
courseRegistrationDoc,
|
|
8375
|
+
currentCard
|
|
8376
|
+
);
|
|
8377
|
+
} else {
|
|
8378
|
+
const k = Math.ceil(32 / history.records.length);
|
|
8379
|
+
void this.eloService.updateUserAndCardElo(
|
|
8380
|
+
userScore,
|
|
8381
|
+
courseId,
|
|
8382
|
+
cardId,
|
|
8383
|
+
courseRegistrationDoc,
|
|
8384
|
+
currentCard,
|
|
8385
|
+
k
|
|
8386
|
+
);
|
|
8387
|
+
}
|
|
8388
|
+
logger.info(
|
|
8389
|
+
"[ResponseProcessor] Processed correct response with SRS scheduling and ELO update"
|
|
7830
8390
|
);
|
|
7831
8391
|
}
|
|
7832
|
-
logger.info(
|
|
7833
|
-
"[ResponseProcessor] Processed correct response with SRS scheduling and ELO update"
|
|
7834
|
-
);
|
|
7835
8392
|
return {
|
|
7836
8393
|
nextCardAction: "dismiss-success",
|
|
7837
8394
|
shouldLoadNextCard: true,
|
|
7838
8395
|
isCorrect: true,
|
|
7839
|
-
performanceScore:
|
|
8396
|
+
performanceScore: globalScore,
|
|
7840
8397
|
shouldClearFeedbackShadow: true
|
|
7841
8398
|
};
|
|
7842
8399
|
} else {
|
|
7843
8400
|
logger.info(
|
|
7844
8401
|
"[ResponseProcessor] Processed correct response (retry attempt - no scheduling/ELO)"
|
|
7845
8402
|
);
|
|
8403
|
+
const { globalScore } = this.parsePerformance(cardRecord.performance);
|
|
7846
8404
|
return {
|
|
7847
8405
|
nextCardAction: "marked-failed",
|
|
7848
8406
|
shouldLoadNextCard: true,
|
|
7849
8407
|
isCorrect: true,
|
|
7850
|
-
performanceScore:
|
|
8408
|
+
performanceScore: globalScore,
|
|
7851
8409
|
shouldClearFeedbackShadow: true
|
|
7852
8410
|
};
|
|
7853
8411
|
}
|
|
@@ -7856,28 +8414,52 @@ var ResponseProcessor = class {
|
|
|
7856
8414
|
* Handles processing for incorrect responses: ELO updates only.
|
|
7857
8415
|
*/
|
|
7858
8416
|
processIncorrectResponse(cardRecord, history, courseRegistrationDoc, currentCard, courseId, cardId, maxAttemptsPerView, maxSessionViews, sessionViews) {
|
|
8417
|
+
const { taggedPerformance } = this.parsePerformance(cardRecord.performance);
|
|
7859
8418
|
if (history.records.length !== 1 && cardRecord.priorAttemps === 0) {
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
7863
|
-
|
|
7864
|
-
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
|
|
7870
|
-
|
|
7871
|
-
|
|
7872
|
-
if (currentCard.records.length >= maxAttemptsPerView) {
|
|
7873
|
-
if (sessionViews >= maxSessionViews) {
|
|
8419
|
+
if (taggedPerformance) {
|
|
8420
|
+
void this.eloService.updateUserAndCardEloPerTag(
|
|
8421
|
+
taggedPerformance,
|
|
8422
|
+
courseId,
|
|
8423
|
+
cardId,
|
|
8424
|
+
courseRegistrationDoc,
|
|
8425
|
+
currentCard
|
|
8426
|
+
);
|
|
8427
|
+
logger.info(
|
|
8428
|
+
`[ResponseProcessor] Processed incorrect response with per-tag ELO update (${Object.keys(taggedPerformance).length - 1} tags)`
|
|
8429
|
+
);
|
|
8430
|
+
} else {
|
|
7874
8431
|
void this.eloService.updateUserAndCardElo(
|
|
7875
8432
|
0,
|
|
8433
|
+
// Failed response = 0 score
|
|
7876
8434
|
courseId,
|
|
7877
8435
|
cardId,
|
|
7878
8436
|
courseRegistrationDoc,
|
|
7879
8437
|
currentCard
|
|
7880
8438
|
);
|
|
8439
|
+
logger.info("[ResponseProcessor] Processed incorrect response with ELO update");
|
|
8440
|
+
}
|
|
8441
|
+
} else {
|
|
8442
|
+
logger.info("[ResponseProcessor] Processed incorrect response (no ELO update needed)");
|
|
8443
|
+
}
|
|
8444
|
+
if (currentCard.records.length >= maxAttemptsPerView) {
|
|
8445
|
+
if (sessionViews >= maxSessionViews) {
|
|
8446
|
+
if (taggedPerformance) {
|
|
8447
|
+
void this.eloService.updateUserAndCardEloPerTag(
|
|
8448
|
+
taggedPerformance,
|
|
8449
|
+
courseId,
|
|
8450
|
+
cardId,
|
|
8451
|
+
courseRegistrationDoc,
|
|
8452
|
+
currentCard
|
|
8453
|
+
);
|
|
8454
|
+
} else {
|
|
8455
|
+
void this.eloService.updateUserAndCardElo(
|
|
8456
|
+
0,
|
|
8457
|
+
courseId,
|
|
8458
|
+
cardId,
|
|
8459
|
+
courseRegistrationDoc,
|
|
8460
|
+
currentCard
|
|
8461
|
+
);
|
|
8462
|
+
}
|
|
7881
8463
|
return {
|
|
7882
8464
|
nextCardAction: "dismiss-failed",
|
|
7883
8465
|
shouldLoadNextCard: true,
|
|
@@ -7904,7 +8486,7 @@ var ResponseProcessor = class {
|
|
|
7904
8486
|
};
|
|
7905
8487
|
|
|
7906
8488
|
// src/study/services/CardHydrationService.ts
|
|
7907
|
-
var
|
|
8489
|
+
var import_common24 = require("@vue-skuilder/common");
|
|
7908
8490
|
init_logger();
|
|
7909
8491
|
function parseAudioURIs(data) {
|
|
7910
8492
|
if (typeof data !== "string") return [];
|
|
@@ -8039,8 +8621,8 @@ var CardHydrationService = class {
|
|
|
8039
8621
|
try {
|
|
8040
8622
|
const courseDB = this.getCourseDB(item.courseID);
|
|
8041
8623
|
const cardData = await courseDB.getCourseDoc(item.cardID);
|
|
8042
|
-
if (!(0,
|
|
8043
|
-
cardData.elo = (0,
|
|
8624
|
+
if (!(0, import_common24.isCourseElo)(cardData.elo)) {
|
|
8625
|
+
cardData.elo = (0, import_common24.toCourseElo)(cardData.elo);
|
|
8044
8626
|
}
|
|
8045
8627
|
const view = this.getViewComponent(cardData.id_view);
|
|
8046
8628
|
const dataDocs = await Promise.all(
|
|
@@ -8064,7 +8646,7 @@ var CardHydrationService = class {
|
|
|
8064
8646
|
);
|
|
8065
8647
|
await Promise.allSettled(uniqueAudioUrls.map(prefetchAudio));
|
|
8066
8648
|
}
|
|
8067
|
-
const data = dataDocs.map(
|
|
8649
|
+
const data = dataDocs.map(import_common24.displayableDataToViewData).reverse();
|
|
8068
8650
|
this.hydratedCards.set(item.cardID, {
|
|
8069
8651
|
item,
|
|
8070
8652
|
view,
|
|
@@ -9559,16 +10141,645 @@ var QuotaRoundRobinMixer = class {
|
|
|
9559
10141
|
return [];
|
|
9560
10142
|
}
|
|
9561
10143
|
const quotaPerSource = Math.ceil(limit / batches.length);
|
|
9562
|
-
const
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
10144
|
+
const sourceStacks = batches.map((batch) => {
|
|
10145
|
+
return [...batch.weighted].sort((a, b) => b.score - a.score).slice(0, quotaPerSource);
|
|
10146
|
+
});
|
|
10147
|
+
for (let i = sourceStacks.length - 1; i > 0; i--) {
|
|
10148
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
10149
|
+
[sourceStacks[i], sourceStacks[j]] = [sourceStacks[j], sourceStacks[i]];
|
|
10150
|
+
}
|
|
10151
|
+
const result = [];
|
|
10152
|
+
let exhausted = 0;
|
|
10153
|
+
const cursors = new Array(sourceStacks.length).fill(0);
|
|
10154
|
+
while (result.length < limit && exhausted < sourceStacks.length) {
|
|
10155
|
+
exhausted = 0;
|
|
10156
|
+
for (let s = 0; s < sourceStacks.length; s++) {
|
|
10157
|
+
if (result.length >= limit) break;
|
|
10158
|
+
if (cursors[s] < sourceStacks[s].length) {
|
|
10159
|
+
result.push(sourceStacks[s][cursors[s]]);
|
|
10160
|
+
cursors[s]++;
|
|
10161
|
+
} else {
|
|
10162
|
+
exhausted++;
|
|
10163
|
+
}
|
|
10164
|
+
}
|
|
9567
10165
|
}
|
|
9568
|
-
return
|
|
10166
|
+
return result;
|
|
9569
10167
|
}
|
|
9570
10168
|
};
|
|
9571
10169
|
|
|
10170
|
+
// src/study/MixerDebugger.ts
|
|
10171
|
+
init_logger();
|
|
10172
|
+
init_navigators();
|
|
10173
|
+
var MAX_RUNS2 = 10;
|
|
10174
|
+
var runHistory2 = [];
|
|
10175
|
+
function buildSourceSummary(batch, sourceId, sourceName) {
|
|
10176
|
+
const scores = batch.weighted.map((c) => c.score);
|
|
10177
|
+
const reviewCount = batch.weighted.filter((c) => getCardOrigin(c) === "review").length;
|
|
10178
|
+
const newCount = batch.weighted.filter((c) => getCardOrigin(c) === "new").length;
|
|
10179
|
+
return {
|
|
10180
|
+
sourceIndex: batch.sourceIndex,
|
|
10181
|
+
sourceId,
|
|
10182
|
+
sourceName,
|
|
10183
|
+
totalCards: batch.weighted.length,
|
|
10184
|
+
reviewCount,
|
|
10185
|
+
newCount,
|
|
10186
|
+
topScore: scores.length > 0 ? Math.max(...scores) : 0,
|
|
10187
|
+
bottomScore: scores.length > 0 ? Math.min(...scores) : 0,
|
|
10188
|
+
scoreRange: scores.length > 0 ? [Math.min(...scores), Math.max(...scores)] : [0, 0],
|
|
10189
|
+
avgScore: scores.length > 0 ? scores.reduce((a, b) => a + b, 0) / scores.length : 0
|
|
10190
|
+
};
|
|
10191
|
+
}
|
|
10192
|
+
function buildSourceBreakdown(sourceId, sourceName, allCards) {
|
|
10193
|
+
const sourceCards = allCards.filter((c) => c.courseId === sourceId);
|
|
10194
|
+
const selectedCards = sourceCards.filter((c) => c.selected);
|
|
10195
|
+
const reviewsProvided = sourceCards.filter((c) => c.origin === "review").length;
|
|
10196
|
+
const newProvided = sourceCards.filter((c) => c.origin === "new").length;
|
|
10197
|
+
const reviewsSelected = selectedCards.filter((c) => c.origin === "review").length;
|
|
10198
|
+
const newSelected = selectedCards.filter((c) => c.origin === "new").length;
|
|
10199
|
+
return {
|
|
10200
|
+
sourceId,
|
|
10201
|
+
sourceName,
|
|
10202
|
+
reviewsProvided,
|
|
10203
|
+
newProvided,
|
|
10204
|
+
reviewsSelected,
|
|
10205
|
+
newSelected,
|
|
10206
|
+
totalSelected: selectedCards.length,
|
|
10207
|
+
selectionRate: sourceCards.length > 0 ? selectedCards.length / sourceCards.length * 100 : 0
|
|
10208
|
+
};
|
|
10209
|
+
}
|
|
10210
|
+
function captureMixerRun(mixerType, batches, sourceIds, sourceNames, requestedLimit, quotaPerSource, mixedResult) {
|
|
10211
|
+
const sourceSummaries = batches.map(
|
|
10212
|
+
(batch, idx) => buildSourceSummary(batch, sourceIds[idx] || `source-${idx}`, sourceNames[idx])
|
|
10213
|
+
);
|
|
10214
|
+
const selectedIds = new Set(mixedResult.map((c) => c.cardId));
|
|
10215
|
+
const sourceRankings = /* @__PURE__ */ new Map();
|
|
10216
|
+
batches.forEach((batch) => {
|
|
10217
|
+
const sorted = [...batch.weighted].sort((a, b) => b.score - a.score);
|
|
10218
|
+
const rankings = /* @__PURE__ */ new Map();
|
|
10219
|
+
sorted.forEach((card, idx) => {
|
|
10220
|
+
rankings.set(card.cardId, idx + 1);
|
|
10221
|
+
});
|
|
10222
|
+
sourceRankings.set(sourceIds[batch.sourceIndex] || `source-${batch.sourceIndex}`, rankings);
|
|
10223
|
+
});
|
|
10224
|
+
const mixRankings = /* @__PURE__ */ new Map();
|
|
10225
|
+
mixedResult.forEach((card, idx) => {
|
|
10226
|
+
mixRankings.set(card.cardId, idx + 1);
|
|
10227
|
+
});
|
|
10228
|
+
const allCardsMap = /* @__PURE__ */ new Map();
|
|
10229
|
+
batches.forEach((batch) => {
|
|
10230
|
+
batch.weighted.forEach((card) => {
|
|
10231
|
+
allCardsMap.set(card.cardId, card);
|
|
10232
|
+
});
|
|
10233
|
+
});
|
|
10234
|
+
const cards = Array.from(allCardsMap.values()).map((card) => ({
|
|
10235
|
+
cardId: card.cardId,
|
|
10236
|
+
courseId: card.courseId,
|
|
10237
|
+
origin: getCardOrigin(card),
|
|
10238
|
+
score: card.score,
|
|
10239
|
+
sourceIndex: batches.findIndex((b) => b.weighted.some((c) => c.cardId === card.cardId)),
|
|
10240
|
+
selected: selectedIds.has(card.cardId),
|
|
10241
|
+
rankInSource: sourceRankings.get(card.courseId)?.get(card.cardId),
|
|
10242
|
+
rankInMix: mixRankings.get(card.cardId)
|
|
10243
|
+
}));
|
|
10244
|
+
const uniqueSourceIds = Array.from(new Set(sourceIds.filter((id) => id)));
|
|
10245
|
+
const sourceBreakdowns = uniqueSourceIds.map(
|
|
10246
|
+
(sourceId, idx) => buildSourceBreakdown(sourceId, sourceNames[idx], cards)
|
|
10247
|
+
);
|
|
10248
|
+
const report = {
|
|
10249
|
+
runId: `mix-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
10250
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
10251
|
+
mixerType,
|
|
10252
|
+
requestedLimit,
|
|
10253
|
+
quotaPerSource,
|
|
10254
|
+
sourceSummaries,
|
|
10255
|
+
cards,
|
|
10256
|
+
finalCount: mixedResult.length,
|
|
10257
|
+
reviewsSelected: mixedResult.filter((c) => getCardOrigin(c) === "review").length,
|
|
10258
|
+
newSelected: mixedResult.filter((c) => getCardOrigin(c) === "new").length,
|
|
10259
|
+
sourceBreakdowns
|
|
10260
|
+
};
|
|
10261
|
+
runHistory2.unshift(report);
|
|
10262
|
+
if (runHistory2.length > MAX_RUNS2) {
|
|
10263
|
+
runHistory2.pop();
|
|
10264
|
+
}
|
|
10265
|
+
}
|
|
10266
|
+
function printMixerSummary(run) {
|
|
10267
|
+
console.group(`\u{1F3A8} Mixer Run: ${run.mixerType}`);
|
|
10268
|
+
logger.info(`Run ID: ${run.runId}`);
|
|
10269
|
+
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
10270
|
+
logger.info(
|
|
10271
|
+
`Config: limit=${run.requestedLimit}${run.quotaPerSource ? `, quota/source=${run.quotaPerSource}` : ""}`
|
|
10272
|
+
);
|
|
10273
|
+
console.group(`\u{1F4E5} Input: ${run.sourceSummaries.length} sources`);
|
|
10274
|
+
for (const src of run.sourceSummaries) {
|
|
10275
|
+
logger.info(
|
|
10276
|
+
` ${src.sourceName || src.sourceId}: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`
|
|
10277
|
+
);
|
|
10278
|
+
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}], avg: ${src.avgScore.toFixed(2)}`);
|
|
10279
|
+
}
|
|
10280
|
+
console.groupEnd();
|
|
10281
|
+
console.group(`\u{1F4E4} Output: ${run.finalCount} cards selected (${run.reviewsSelected} reviews, ${run.newSelected} new)`);
|
|
10282
|
+
for (const breakdown of run.sourceBreakdowns) {
|
|
10283
|
+
const name = breakdown.sourceName || breakdown.sourceId;
|
|
10284
|
+
logger.info(
|
|
10285
|
+
` ${name}: ${breakdown.totalSelected} selected (${breakdown.reviewsSelected} reviews, ${breakdown.newSelected} new) - ${breakdown.selectionRate.toFixed(1)}% selection rate`
|
|
10286
|
+
);
|
|
10287
|
+
}
|
|
10288
|
+
console.groupEnd();
|
|
10289
|
+
console.groupEnd();
|
|
10290
|
+
}
|
|
10291
|
+
var mixerDebugAPI = {
|
|
10292
|
+
/**
|
|
10293
|
+
* Get raw run history for programmatic access.
|
|
10294
|
+
*/
|
|
10295
|
+
get runs() {
|
|
10296
|
+
return [...runHistory2];
|
|
10297
|
+
},
|
|
10298
|
+
/**
|
|
10299
|
+
* Show summary of a specific mixer run.
|
|
10300
|
+
*/
|
|
10301
|
+
showRun(idOrIndex = 0) {
|
|
10302
|
+
if (runHistory2.length === 0) {
|
|
10303
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
10304
|
+
return;
|
|
10305
|
+
}
|
|
10306
|
+
let run;
|
|
10307
|
+
if (typeof idOrIndex === "number") {
|
|
10308
|
+
run = runHistory2[idOrIndex];
|
|
10309
|
+
if (!run) {
|
|
10310
|
+
logger.info(`[Mixer Debug] No run found at index ${idOrIndex}. History length: ${runHistory2.length}`);
|
|
10311
|
+
return;
|
|
10312
|
+
}
|
|
10313
|
+
} else {
|
|
10314
|
+
run = runHistory2.find((r) => r.runId.endsWith(idOrIndex));
|
|
10315
|
+
if (!run) {
|
|
10316
|
+
logger.info(`[Mixer Debug] No run found matching ID '${idOrIndex}'.`);
|
|
10317
|
+
return;
|
|
10318
|
+
}
|
|
10319
|
+
}
|
|
10320
|
+
printMixerSummary(run);
|
|
10321
|
+
},
|
|
10322
|
+
/**
|
|
10323
|
+
* Show summary of the last mixer run.
|
|
10324
|
+
*/
|
|
10325
|
+
showLastMix() {
|
|
10326
|
+
this.showRun(0);
|
|
10327
|
+
},
|
|
10328
|
+
/**
|
|
10329
|
+
* Explain source balance in the last run.
|
|
10330
|
+
*/
|
|
10331
|
+
explainSourceBalance() {
|
|
10332
|
+
if (runHistory2.length === 0) {
|
|
10333
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
10334
|
+
return;
|
|
10335
|
+
}
|
|
10336
|
+
const run = runHistory2[0];
|
|
10337
|
+
console.group("\u2696\uFE0F Source Balance Analysis");
|
|
10338
|
+
logger.info(`Mixer: ${run.mixerType}`);
|
|
10339
|
+
logger.info(`Requested limit: ${run.requestedLimit}`);
|
|
10340
|
+
if (run.quotaPerSource) {
|
|
10341
|
+
logger.info(`Quota per source: ${run.quotaPerSource}`);
|
|
10342
|
+
}
|
|
10343
|
+
console.group("Input Distribution:");
|
|
10344
|
+
for (const src of run.sourceSummaries) {
|
|
10345
|
+
const name = src.sourceName || src.sourceId;
|
|
10346
|
+
logger.info(`${name}:`);
|
|
10347
|
+
logger.info(` Provided: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`);
|
|
10348
|
+
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}]`);
|
|
10349
|
+
}
|
|
10350
|
+
console.groupEnd();
|
|
10351
|
+
console.group("Selection Results:");
|
|
10352
|
+
for (const breakdown of run.sourceBreakdowns) {
|
|
10353
|
+
const name = breakdown.sourceName || breakdown.sourceId;
|
|
10354
|
+
logger.info(`${name}:`);
|
|
10355
|
+
logger.info(
|
|
10356
|
+
` Selected: ${breakdown.totalSelected}/${breakdown.reviewsProvided + breakdown.newProvided} (${breakdown.selectionRate.toFixed(1)}%)`
|
|
10357
|
+
);
|
|
10358
|
+
logger.info(` Reviews: ${breakdown.reviewsSelected}/${breakdown.reviewsProvided}`);
|
|
10359
|
+
logger.info(` New: ${breakdown.newSelected}/${breakdown.newProvided}`);
|
|
10360
|
+
if (breakdown.reviewsProvided > 0 && breakdown.reviewsSelected === 0) {
|
|
10361
|
+
logger.info(` \u26A0\uFE0F Had reviews but none selected!`);
|
|
10362
|
+
}
|
|
10363
|
+
if (breakdown.totalSelected === 0 && breakdown.reviewsProvided + breakdown.newProvided > 0) {
|
|
10364
|
+
logger.info(` \u26A0\uFE0F Had cards but none selected!`);
|
|
10365
|
+
}
|
|
10366
|
+
}
|
|
10367
|
+
console.groupEnd();
|
|
10368
|
+
const selectionRates = run.sourceBreakdowns.map((b) => b.selectionRate);
|
|
10369
|
+
const avgRate = selectionRates.reduce((a, b) => a + b, 0) / selectionRates.length;
|
|
10370
|
+
const maxDeviation = Math.max(...selectionRates.map((r) => Math.abs(r - avgRate)));
|
|
10371
|
+
if (maxDeviation > 20) {
|
|
10372
|
+
logger.info(`
|
|
10373
|
+
\u26A0\uFE0F Significant imbalance detected (max deviation: ${maxDeviation.toFixed(1)}%)`);
|
|
10374
|
+
logger.info("Possible causes:");
|
|
10375
|
+
logger.info(" - Score range differences between sources");
|
|
10376
|
+
logger.info(" - One source has much better quality cards");
|
|
10377
|
+
logger.info(" - Different card availability (reviews vs new)");
|
|
10378
|
+
}
|
|
10379
|
+
console.groupEnd();
|
|
10380
|
+
},
|
|
10381
|
+
/**
|
|
10382
|
+
* Compare score distributions across sources.
|
|
10383
|
+
*/
|
|
10384
|
+
compareScores() {
|
|
10385
|
+
if (runHistory2.length === 0) {
|
|
10386
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
10387
|
+
return;
|
|
10388
|
+
}
|
|
10389
|
+
const run = runHistory2[0];
|
|
10390
|
+
console.group("\u{1F4CA} Score Distribution Comparison");
|
|
10391
|
+
console.table(
|
|
10392
|
+
run.sourceSummaries.map((src) => ({
|
|
10393
|
+
source: src.sourceName || src.sourceId,
|
|
10394
|
+
cards: src.totalCards,
|
|
10395
|
+
min: src.bottomScore.toFixed(3),
|
|
10396
|
+
max: src.topScore.toFixed(3),
|
|
10397
|
+
avg: src.avgScore.toFixed(3),
|
|
10398
|
+
range: (src.topScore - src.bottomScore).toFixed(3)
|
|
10399
|
+
}))
|
|
10400
|
+
);
|
|
10401
|
+
const ranges = run.sourceSummaries.map((s) => s.topScore - s.bottomScore);
|
|
10402
|
+
const avgScores = run.sourceSummaries.map((s) => s.avgScore);
|
|
10403
|
+
const rangeDiff = Math.max(...ranges) - Math.min(...ranges);
|
|
10404
|
+
const avgDiff = Math.max(...avgScores) - Math.min(...avgScores);
|
|
10405
|
+
if (rangeDiff > 0.3 || avgDiff > 0.2) {
|
|
10406
|
+
logger.info("\n\u26A0\uFE0F Significant score distribution differences detected");
|
|
10407
|
+
logger.info(
|
|
10408
|
+
"This may cause one source to dominate selection if using global sorting (not quota-based)"
|
|
10409
|
+
);
|
|
10410
|
+
}
|
|
10411
|
+
console.groupEnd();
|
|
10412
|
+
},
|
|
10413
|
+
/**
|
|
10414
|
+
* Show detailed information for a specific card.
|
|
10415
|
+
*/
|
|
10416
|
+
showCard(cardId) {
|
|
10417
|
+
for (const run of runHistory2) {
|
|
10418
|
+
const card = run.cards.find((c) => c.cardId === cardId);
|
|
10419
|
+
if (card) {
|
|
10420
|
+
const source = run.sourceSummaries.find((s) => s.sourceIndex === card.sourceIndex);
|
|
10421
|
+
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
10422
|
+
logger.info(`Course: ${card.courseId}`);
|
|
10423
|
+
logger.info(`Source: ${source?.sourceName || source?.sourceId || "unknown"}`);
|
|
10424
|
+
logger.info(`Origin: ${card.origin}`);
|
|
10425
|
+
logger.info(`Score: ${card.score.toFixed(3)}`);
|
|
10426
|
+
if (card.rankInSource) {
|
|
10427
|
+
logger.info(`Rank in source: #${card.rankInSource}`);
|
|
10428
|
+
}
|
|
10429
|
+
if (card.rankInMix) {
|
|
10430
|
+
logger.info(`Rank in mixed results: #${card.rankInMix}`);
|
|
10431
|
+
}
|
|
10432
|
+
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
10433
|
+
if (!card.selected && card.rankInSource) {
|
|
10434
|
+
logger.info("\nWhy not selected:");
|
|
10435
|
+
if (run.quotaPerSource && card.rankInSource > run.quotaPerSource) {
|
|
10436
|
+
logger.info(` - Ranked #${card.rankInSource} in source, but quota was ${run.quotaPerSource}`);
|
|
10437
|
+
}
|
|
10438
|
+
logger.info(" - Check score compared to selected cards using .showRun()");
|
|
10439
|
+
}
|
|
10440
|
+
console.groupEnd();
|
|
10441
|
+
return;
|
|
10442
|
+
}
|
|
10443
|
+
}
|
|
10444
|
+
logger.info(`[Mixer Debug] Card '${cardId}' not found in recent runs.`);
|
|
10445
|
+
},
|
|
10446
|
+
/**
|
|
10447
|
+
* Show all runs in compact format.
|
|
10448
|
+
*/
|
|
10449
|
+
listRuns() {
|
|
10450
|
+
if (runHistory2.length === 0) {
|
|
10451
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
10452
|
+
return;
|
|
10453
|
+
}
|
|
10454
|
+
console.table(
|
|
10455
|
+
runHistory2.map((r) => ({
|
|
10456
|
+
id: r.runId.slice(-8),
|
|
10457
|
+
time: r.timestamp.toLocaleTimeString(),
|
|
10458
|
+
mixer: r.mixerType,
|
|
10459
|
+
sources: r.sourceSummaries.length,
|
|
10460
|
+
selected: r.finalCount,
|
|
10461
|
+
reviews: r.reviewsSelected,
|
|
10462
|
+
new: r.newSelected
|
|
10463
|
+
}))
|
|
10464
|
+
);
|
|
10465
|
+
},
|
|
10466
|
+
/**
|
|
10467
|
+
* Export run history as JSON for bug reports.
|
|
10468
|
+
*/
|
|
10469
|
+
export() {
|
|
10470
|
+
const json = JSON.stringify(runHistory2, null, 2);
|
|
10471
|
+
logger.info("[Mixer Debug] Run history exported. Copy the returned string or use:");
|
|
10472
|
+
logger.info(" copy(window.skuilder.mixer.export())");
|
|
10473
|
+
return json;
|
|
10474
|
+
},
|
|
10475
|
+
/**
|
|
10476
|
+
* Clear run history.
|
|
10477
|
+
*/
|
|
10478
|
+
clear() {
|
|
10479
|
+
runHistory2.length = 0;
|
|
10480
|
+
logger.info("[Mixer Debug] Run history cleared.");
|
|
10481
|
+
},
|
|
10482
|
+
/**
|
|
10483
|
+
* Show help.
|
|
10484
|
+
*/
|
|
10485
|
+
help() {
|
|
10486
|
+
logger.info(`
|
|
10487
|
+
\u{1F3A8} Mixer Debug API
|
|
10488
|
+
|
|
10489
|
+
Commands:
|
|
10490
|
+
.showLastMix() Show summary of most recent mixer run
|
|
10491
|
+
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
10492
|
+
.explainSourceBalance() Analyze source balance and selection patterns
|
|
10493
|
+
.compareScores() Compare score distributions across sources
|
|
10494
|
+
.showCard(cardId) Show mixer decisions for a specific card
|
|
10495
|
+
.listRuns() List all captured runs in table format
|
|
10496
|
+
.export() Export run history as JSON for bug reports
|
|
10497
|
+
.clear() Clear run history
|
|
10498
|
+
.runs Access raw run history array
|
|
10499
|
+
.help() Show this help message
|
|
10500
|
+
|
|
10501
|
+
Example:
|
|
10502
|
+
window.skuilder.mixer.showLastMix()
|
|
10503
|
+
window.skuilder.mixer.explainSourceBalance()
|
|
10504
|
+
window.skuilder.mixer.compareScores()
|
|
10505
|
+
`);
|
|
10506
|
+
}
|
|
10507
|
+
};
|
|
10508
|
+
function mountMixerDebugger() {
|
|
10509
|
+
if (typeof window === "undefined") return;
|
|
10510
|
+
const win = window;
|
|
10511
|
+
win.skuilder = win.skuilder || {};
|
|
10512
|
+
win.skuilder.mixer = mixerDebugAPI;
|
|
10513
|
+
}
|
|
10514
|
+
mountMixerDebugger();
|
|
10515
|
+
|
|
10516
|
+
// src/study/SessionDebugger.ts
|
|
10517
|
+
init_logger();
|
|
10518
|
+
var activeSession = null;
|
|
10519
|
+
var sessionHistory = [];
|
|
10520
|
+
var MAX_HISTORY = 5;
|
|
10521
|
+
function startSessionTracking(reviewQLength, newQLength, failedQLength) {
|
|
10522
|
+
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
10523
|
+
activeSession = {
|
|
10524
|
+
sessionId,
|
|
10525
|
+
startTime: /* @__PURE__ */ new Date(),
|
|
10526
|
+
initialQueues: {
|
|
10527
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
10528
|
+
reviewQLength,
|
|
10529
|
+
newQLength,
|
|
10530
|
+
failedQLength
|
|
10531
|
+
},
|
|
10532
|
+
presentations: [],
|
|
10533
|
+
queueSnapshots: []
|
|
10534
|
+
};
|
|
10535
|
+
logger.debug(`[SessionDebugger] Started tracking session: ${sessionId}`);
|
|
10536
|
+
}
|
|
10537
|
+
function recordCardPresentation(cardId, courseId, courseName, origin, queueSource, score) {
|
|
10538
|
+
if (!activeSession) {
|
|
10539
|
+
logger.warn("[SessionDebugger] No active session to record presentation");
|
|
10540
|
+
return;
|
|
10541
|
+
}
|
|
10542
|
+
activeSession.presentations.push({
|
|
10543
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
10544
|
+
sequenceNumber: activeSession.presentations.length + 1,
|
|
10545
|
+
cardId,
|
|
10546
|
+
courseId,
|
|
10547
|
+
courseName,
|
|
10548
|
+
origin,
|
|
10549
|
+
queueSource,
|
|
10550
|
+
score
|
|
10551
|
+
});
|
|
10552
|
+
}
|
|
10553
|
+
function snapshotQueues(reviewQLength, newQLength, failedQLength, reviewQNext3, newQNext3) {
|
|
10554
|
+
if (!activeSession) {
|
|
10555
|
+
return;
|
|
10556
|
+
}
|
|
10557
|
+
activeSession.queueSnapshots.push({
|
|
10558
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
10559
|
+
reviewQLength,
|
|
10560
|
+
newQLength,
|
|
10561
|
+
failedQLength,
|
|
10562
|
+
reviewQNext3,
|
|
10563
|
+
newQNext3
|
|
10564
|
+
});
|
|
10565
|
+
}
|
|
10566
|
+
function endSessionTracking() {
|
|
10567
|
+
if (!activeSession) {
|
|
10568
|
+
return;
|
|
10569
|
+
}
|
|
10570
|
+
activeSession.endTime = /* @__PURE__ */ new Date();
|
|
10571
|
+
sessionHistory.unshift(activeSession);
|
|
10572
|
+
if (sessionHistory.length > MAX_HISTORY) {
|
|
10573
|
+
sessionHistory.pop();
|
|
10574
|
+
}
|
|
10575
|
+
logger.debug(`[SessionDebugger] Ended session: ${activeSession.sessionId}`);
|
|
10576
|
+
activeSession = null;
|
|
10577
|
+
}
|
|
10578
|
+
function showCurrentQueue() {
|
|
10579
|
+
if (!activeSession) {
|
|
10580
|
+
logger.info("[Session Debug] No active session.");
|
|
10581
|
+
return;
|
|
10582
|
+
}
|
|
10583
|
+
const latest = activeSession.queueSnapshots[activeSession.queueSnapshots.length - 1] || activeSession.initialQueues;
|
|
10584
|
+
console.group("\u{1F4CA} Current Queue State");
|
|
10585
|
+
logger.info(`Review Queue: ${latest.reviewQLength} cards`);
|
|
10586
|
+
if (latest.reviewQNext3 && latest.reviewQNext3.length > 0) {
|
|
10587
|
+
logger.info(` Next: ${latest.reviewQNext3.join(", ")}`);
|
|
10588
|
+
}
|
|
10589
|
+
logger.info(`New Queue: ${latest.newQLength} cards`);
|
|
10590
|
+
if (latest.newQNext3 && latest.newQNext3.length > 0) {
|
|
10591
|
+
logger.info(` Next: ${latest.newQNext3.join(", ")}`);
|
|
10592
|
+
}
|
|
10593
|
+
logger.info(`Failed Queue: ${latest.failedQLength} cards`);
|
|
10594
|
+
console.groupEnd();
|
|
10595
|
+
}
|
|
10596
|
+
function showPresentationHistory(sessionIndex = 0) {
|
|
10597
|
+
const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
|
|
10598
|
+
if (!session) {
|
|
10599
|
+
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
10600
|
+
return;
|
|
10601
|
+
}
|
|
10602
|
+
console.group(`\u{1F4DC} Session History: ${session.sessionId}`);
|
|
10603
|
+
logger.info(`Started: ${session.startTime.toLocaleTimeString()}`);
|
|
10604
|
+
if (session.endTime) {
|
|
10605
|
+
logger.info(`Ended: ${session.endTime.toLocaleTimeString()}`);
|
|
10606
|
+
}
|
|
10607
|
+
logger.info(`Cards presented: ${session.presentations.length}`);
|
|
10608
|
+
if (session.presentations.length > 0) {
|
|
10609
|
+
console.table(
|
|
10610
|
+
session.presentations.map((p) => ({
|
|
10611
|
+
"#": p.sequenceNumber,
|
|
10612
|
+
course: p.courseName || p.courseId.slice(0, 8),
|
|
10613
|
+
origin: p.origin,
|
|
10614
|
+
queue: p.queueSource,
|
|
10615
|
+
score: p.score?.toFixed(3) || "-",
|
|
10616
|
+
time: p.timestamp.toLocaleTimeString()
|
|
10617
|
+
}))
|
|
10618
|
+
);
|
|
10619
|
+
}
|
|
10620
|
+
console.groupEnd();
|
|
10621
|
+
}
|
|
10622
|
+
function showInterleaving(sessionIndex = 0) {
|
|
10623
|
+
const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
|
|
10624
|
+
if (!session) {
|
|
10625
|
+
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
10626
|
+
return;
|
|
10627
|
+
}
|
|
10628
|
+
console.group("\u{1F500} Interleaving Analysis");
|
|
10629
|
+
const courseCounts = /* @__PURE__ */ new Map();
|
|
10630
|
+
const courseOrigins = /* @__PURE__ */ new Map();
|
|
10631
|
+
session.presentations.forEach((p) => {
|
|
10632
|
+
const name = p.courseName || p.courseId;
|
|
10633
|
+
courseCounts.set(name, (courseCounts.get(name) || 0) + 1);
|
|
10634
|
+
if (!courseOrigins.has(name)) {
|
|
10635
|
+
courseOrigins.set(name, { review: 0, new: 0, failed: 0 });
|
|
10636
|
+
}
|
|
10637
|
+
const origins = courseOrigins.get(name);
|
|
10638
|
+
origins[p.origin]++;
|
|
10639
|
+
});
|
|
10640
|
+
logger.info("Course distribution:");
|
|
10641
|
+
console.table(
|
|
10642
|
+
Array.from(courseCounts.entries()).map(([course, count]) => {
|
|
10643
|
+
const origins = courseOrigins.get(course);
|
|
10644
|
+
return {
|
|
10645
|
+
course,
|
|
10646
|
+
total: count,
|
|
10647
|
+
reviews: origins.review,
|
|
10648
|
+
new: origins.new,
|
|
10649
|
+
failed: origins.failed,
|
|
10650
|
+
percentage: (count / session.presentations.length * 100).toFixed(1) + "%"
|
|
10651
|
+
};
|
|
10652
|
+
})
|
|
10653
|
+
);
|
|
10654
|
+
if (session.presentations.length > 0) {
|
|
10655
|
+
logger.info("\nPresentation sequence (first 20):");
|
|
10656
|
+
const sequence = session.presentations.slice(0, 20).map((p, idx) => `${idx + 1}. ${p.courseName || p.courseId.slice(0, 8)} (${p.origin})`).join("\n");
|
|
10657
|
+
logger.info(sequence);
|
|
10658
|
+
}
|
|
10659
|
+
let maxCluster = 0;
|
|
10660
|
+
let currentCluster = 1;
|
|
10661
|
+
let currentCourse = session.presentations[0]?.courseId;
|
|
10662
|
+
for (let i = 1; i < session.presentations.length; i++) {
|
|
10663
|
+
if (session.presentations[i].courseId === currentCourse) {
|
|
10664
|
+
currentCluster++;
|
|
10665
|
+
maxCluster = Math.max(maxCluster, currentCluster);
|
|
10666
|
+
} else {
|
|
10667
|
+
currentCourse = session.presentations[i].courseId;
|
|
10668
|
+
currentCluster = 1;
|
|
10669
|
+
}
|
|
10670
|
+
}
|
|
10671
|
+
if (maxCluster > 3) {
|
|
10672
|
+
logger.info(`
|
|
10673
|
+
\u26A0\uFE0F Detected clustering: max ${maxCluster} cards from same course in a row`);
|
|
10674
|
+
logger.info("This suggests cards are sorted by score rather than round-robin by course.");
|
|
10675
|
+
}
|
|
10676
|
+
console.groupEnd();
|
|
10677
|
+
}
|
|
10678
|
+
var sessionDebugAPI = {
|
|
10679
|
+
/**
|
|
10680
|
+
* Get raw session history for programmatic access.
|
|
10681
|
+
*/
|
|
10682
|
+
get sessions() {
|
|
10683
|
+
return [...sessionHistory];
|
|
10684
|
+
},
|
|
10685
|
+
/**
|
|
10686
|
+
* Get active session if any.
|
|
10687
|
+
*/
|
|
10688
|
+
get active() {
|
|
10689
|
+
return activeSession;
|
|
10690
|
+
},
|
|
10691
|
+
/**
|
|
10692
|
+
* Show current queue state.
|
|
10693
|
+
*/
|
|
10694
|
+
showQueue() {
|
|
10695
|
+
showCurrentQueue();
|
|
10696
|
+
},
|
|
10697
|
+
/**
|
|
10698
|
+
* Show presentation history for current or past session.
|
|
10699
|
+
*/
|
|
10700
|
+
showHistory(sessionIndex = 0) {
|
|
10701
|
+
showPresentationHistory(sessionIndex);
|
|
10702
|
+
},
|
|
10703
|
+
/**
|
|
10704
|
+
* Analyze course interleaving pattern.
|
|
10705
|
+
*/
|
|
10706
|
+
showInterleaving(sessionIndex = 0) {
|
|
10707
|
+
showInterleaving(sessionIndex);
|
|
10708
|
+
},
|
|
10709
|
+
/**
|
|
10710
|
+
* List all tracked sessions.
|
|
10711
|
+
*/
|
|
10712
|
+
listSessions() {
|
|
10713
|
+
if (activeSession) {
|
|
10714
|
+
logger.info(`Active session: ${activeSession.sessionId} (${activeSession.presentations.length} cards presented)`);
|
|
10715
|
+
}
|
|
10716
|
+
if (sessionHistory.length === 0) {
|
|
10717
|
+
logger.info("[Session Debug] No completed sessions in history.");
|
|
10718
|
+
return;
|
|
10719
|
+
}
|
|
10720
|
+
console.table(
|
|
10721
|
+
sessionHistory.map((s, idx) => ({
|
|
10722
|
+
index: idx,
|
|
10723
|
+
id: s.sessionId.slice(-8),
|
|
10724
|
+
started: s.startTime.toLocaleTimeString(),
|
|
10725
|
+
ended: s.endTime?.toLocaleTimeString() || "incomplete",
|
|
10726
|
+
cards: s.presentations.length
|
|
10727
|
+
}))
|
|
10728
|
+
);
|
|
10729
|
+
},
|
|
10730
|
+
/**
|
|
10731
|
+
* Export session history as JSON for bug reports.
|
|
10732
|
+
*/
|
|
10733
|
+
export() {
|
|
10734
|
+
const data = {
|
|
10735
|
+
active: activeSession,
|
|
10736
|
+
history: sessionHistory
|
|
10737
|
+
};
|
|
10738
|
+
const json = JSON.stringify(data, null, 2);
|
|
10739
|
+
logger.info("[Session Debug] Session data exported. Copy the returned string or use:");
|
|
10740
|
+
logger.info(" copy(window.skuilder.session.export())");
|
|
10741
|
+
return json;
|
|
10742
|
+
},
|
|
10743
|
+
/**
|
|
10744
|
+
* Clear session history.
|
|
10745
|
+
*/
|
|
10746
|
+
clear() {
|
|
10747
|
+
sessionHistory.length = 0;
|
|
10748
|
+
logger.info("[Session Debug] Session history cleared.");
|
|
10749
|
+
},
|
|
10750
|
+
/**
|
|
10751
|
+
* Show help.
|
|
10752
|
+
*/
|
|
10753
|
+
help() {
|
|
10754
|
+
logger.info(`
|
|
10755
|
+
\u{1F3AF} Session Debug API
|
|
10756
|
+
|
|
10757
|
+
Commands:
|
|
10758
|
+
.showQueue() Show current queue state (active session only)
|
|
10759
|
+
.showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
|
|
10760
|
+
.showInterleaving(index?) Analyze course interleaving pattern
|
|
10761
|
+
.listSessions() List all tracked sessions
|
|
10762
|
+
.export() Export session data as JSON for bug reports
|
|
10763
|
+
.clear() Clear session history
|
|
10764
|
+
.sessions Access raw session history array
|
|
10765
|
+
.active Access active session (if any)
|
|
10766
|
+
.help() Show this help message
|
|
10767
|
+
|
|
10768
|
+
Example:
|
|
10769
|
+
window.skuilder.session.showHistory()
|
|
10770
|
+
window.skuilder.session.showInterleaving()
|
|
10771
|
+
window.skuilder.session.showQueue()
|
|
10772
|
+
`);
|
|
10773
|
+
}
|
|
10774
|
+
};
|
|
10775
|
+
function mountSessionDebugger() {
|
|
10776
|
+
if (typeof window === "undefined") return;
|
|
10777
|
+
const win = window;
|
|
10778
|
+
win.skuilder = win.skuilder || {};
|
|
10779
|
+
win.skuilder.session = sessionDebugAPI;
|
|
10780
|
+
}
|
|
10781
|
+
mountSessionDebugger();
|
|
10782
|
+
|
|
9572
10783
|
// src/study/SessionController.ts
|
|
9573
10784
|
init_logger();
|
|
9574
10785
|
var SessionController = class extends Loggable {
|
|
@@ -9578,6 +10789,8 @@ var SessionController = class extends Loggable {
|
|
|
9578
10789
|
eloService;
|
|
9579
10790
|
hydrationService;
|
|
9580
10791
|
mixer;
|
|
10792
|
+
dataLayer;
|
|
10793
|
+
courseNameCache = /* @__PURE__ */ new Map();
|
|
9581
10794
|
sources;
|
|
9582
10795
|
// dataLayer and getViewComponent now injected into CardHydrationService
|
|
9583
10796
|
_sessionRecord = [];
|
|
@@ -9597,7 +10810,11 @@ var SessionController = class extends Loggable {
|
|
|
9597
10810
|
return this._secondsRemaining;
|
|
9598
10811
|
}
|
|
9599
10812
|
get report() {
|
|
9600
|
-
|
|
10813
|
+
const reviewCount = this.reviewQ.dequeueCount;
|
|
10814
|
+
const newCount = this.newQ.dequeueCount;
|
|
10815
|
+
const reviewWord = reviewCount === 1 ? "review" : "reviews";
|
|
10816
|
+
const newCardWord = newCount === 1 ? "new card" : "new cards";
|
|
10817
|
+
return `${reviewCount} ${reviewWord}, ${newCount} ${newCardWord}`;
|
|
9601
10818
|
}
|
|
9602
10819
|
get detailedReport() {
|
|
9603
10820
|
return this.newQ.toString + "\n" + this.reviewQ.toString + "\n" + this.failedQ.toString;
|
|
@@ -9613,6 +10830,7 @@ var SessionController = class extends Loggable {
|
|
|
9613
10830
|
*/
|
|
9614
10831
|
constructor(sources, time, dataLayer, getViewComponent, mixer) {
|
|
9615
10832
|
super();
|
|
10833
|
+
this.dataLayer = dataLayer;
|
|
9616
10834
|
this.mixer = mixer || new QuotaRoundRobinMixer();
|
|
9617
10835
|
this.srsService = new SrsService(dataLayer.getUserDB());
|
|
9618
10836
|
this.eloService = new EloService(dataLayer, dataLayer.getUserDB());
|
|
@@ -9680,6 +10898,7 @@ var SessionController = class extends Loggable {
|
|
|
9680
10898
|
}
|
|
9681
10899
|
await this.getWeightedContent();
|
|
9682
10900
|
await this.hydrationService.ensureHydratedCards();
|
|
10901
|
+
startSessionTracking(this.reviewQ.length, this.newQ.length, this.failedQ.length);
|
|
9683
10902
|
this._intervalHandle = setInterval(() => {
|
|
9684
10903
|
this.tick();
|
|
9685
10904
|
}, 1e3);
|
|
@@ -9775,6 +10994,30 @@ var SessionController = class extends Loggable {
|
|
|
9775
10994
|
);
|
|
9776
10995
|
}
|
|
9777
10996
|
const mixedWeighted = this.mixer.mix(batches, limit * this.sources.length);
|
|
10997
|
+
const sourceIds = batches.map((b) => {
|
|
10998
|
+
const firstCard = b.weighted[0];
|
|
10999
|
+
return firstCard?.courseId || `source-${b.sourceIndex}`;
|
|
11000
|
+
});
|
|
11001
|
+
await Promise.all(
|
|
11002
|
+
sourceIds.map(async (id) => {
|
|
11003
|
+
try {
|
|
11004
|
+
const config = await this.dataLayer.getCoursesDB().getCourseConfig(id);
|
|
11005
|
+
this.courseNameCache.set(id, config.name);
|
|
11006
|
+
} catch {
|
|
11007
|
+
}
|
|
11008
|
+
})
|
|
11009
|
+
);
|
|
11010
|
+
const sourceNames = sourceIds.map((id) => this.courseNameCache.get(id));
|
|
11011
|
+
const quotaPerSource = this.mixer instanceof QuotaRoundRobinMixer ? Math.ceil(limit * this.sources.length / batches.length) : void 0;
|
|
11012
|
+
captureMixerRun(
|
|
11013
|
+
this.mixer.constructor.name,
|
|
11014
|
+
batches,
|
|
11015
|
+
sourceIds,
|
|
11016
|
+
sourceNames,
|
|
11017
|
+
limit * this.sources.length,
|
|
11018
|
+
quotaPerSource,
|
|
11019
|
+
mixedWeighted
|
|
11020
|
+
);
|
|
9778
11021
|
const reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review");
|
|
9779
11022
|
const newWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "new");
|
|
9780
11023
|
logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
|
|
@@ -9883,21 +11126,46 @@ var SessionController = class extends Loggable {
|
|
|
9883
11126
|
this.dismissCurrentCard(action);
|
|
9884
11127
|
if (this._secondsRemaining <= 0 && this.failedQ.length === 0) {
|
|
9885
11128
|
this._currentCard = null;
|
|
11129
|
+
endSessionTracking();
|
|
9886
11130
|
return null;
|
|
9887
11131
|
}
|
|
9888
|
-
const
|
|
9889
|
-
|
|
9890
|
-
|
|
9891
|
-
|
|
9892
|
-
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
11132
|
+
const MAX_SKIP = 20;
|
|
11133
|
+
for (let attempt = 0; attempt < MAX_SKIP; attempt++) {
|
|
11134
|
+
const nextItem = this._selectNextItemToHydrate();
|
|
11135
|
+
if (!nextItem) {
|
|
11136
|
+
this._currentCard = null;
|
|
11137
|
+
endSessionTracking();
|
|
11138
|
+
return null;
|
|
11139
|
+
}
|
|
11140
|
+
let card = this.hydrationService.getHydratedCard(nextItem.cardID);
|
|
11141
|
+
if (!card) {
|
|
11142
|
+
card = await this.hydrationService.waitForCard(nextItem.cardID);
|
|
11143
|
+
}
|
|
11144
|
+
this.removeItemFromQueue(nextItem);
|
|
11145
|
+
if (card) {
|
|
11146
|
+
await this.hydrationService.ensureHydratedCards();
|
|
11147
|
+
this._currentCard = card;
|
|
11148
|
+
const origin = nextItem.status === "review" || nextItem.status === "failed-review" ? "review" : nextItem.status === "new" || nextItem.status === "failed-new" ? "new" : "failed";
|
|
11149
|
+
const queueSource = nextItem.status.startsWith("failed") ? "failedQ" : nextItem.status === "review" ? "reviewQ" : "newQ";
|
|
11150
|
+
recordCardPresentation(
|
|
11151
|
+
nextItem.cardID,
|
|
11152
|
+
nextItem.courseID,
|
|
11153
|
+
this.courseNameCache.get(nextItem.courseID),
|
|
11154
|
+
origin,
|
|
11155
|
+
queueSource
|
|
11156
|
+
);
|
|
11157
|
+
snapshotQueues(this.reviewQ.length, this.newQ.length, this.failedQ.length);
|
|
11158
|
+
return card;
|
|
11159
|
+
}
|
|
11160
|
+
this.log(`Skipping card ${nextItem.cardID}: hydration failed, trying next`);
|
|
11161
|
+
if (isReview(nextItem)) {
|
|
11162
|
+
this.srsService.removeReview(nextItem.reviewID);
|
|
11163
|
+
}
|
|
9896
11164
|
}
|
|
9897
|
-
this.
|
|
9898
|
-
|
|
9899
|
-
|
|
9900
|
-
return
|
|
11165
|
+
this.log(`Exhausted ${MAX_SKIP} skip attempts finding a hydratable card`);
|
|
11166
|
+
this._currentCard = null;
|
|
11167
|
+
endSessionTracking();
|
|
11168
|
+
return null;
|
|
9901
11169
|
}
|
|
9902
11170
|
/**
|
|
9903
11171
|
* Public API for processing user responses to cards.
|
|
@@ -10046,6 +11314,7 @@ init_factory();
|
|
|
10046
11314
|
aggregateOutcomesForGradient,
|
|
10047
11315
|
areQuestionRecords,
|
|
10048
11316
|
buildStrategyStateId,
|
|
11317
|
+
captureMixerRun,
|
|
10049
11318
|
computeDeviation,
|
|
10050
11319
|
computeEffectiveWeight,
|
|
10051
11320
|
computeOutcomeSignal,
|
|
@@ -10053,6 +11322,7 @@ init_factory();
|
|
|
10053
11322
|
computeStrategyGradient,
|
|
10054
11323
|
createOrchestrationContext,
|
|
10055
11324
|
docIsDeleted,
|
|
11325
|
+
endSessionTracking,
|
|
10056
11326
|
ensureAppDataDirectory,
|
|
10057
11327
|
getAppDataDirectory,
|
|
10058
11328
|
getCardHistoryID,
|
|
@@ -10076,9 +11346,15 @@ init_factory();
|
|
|
10076
11346
|
isQuestionTypeRegistered,
|
|
10077
11347
|
isReview,
|
|
10078
11348
|
log,
|
|
11349
|
+
mixerDebugAPI,
|
|
11350
|
+
mountMixerDebugger,
|
|
11351
|
+
mountPipelineDebugger,
|
|
11352
|
+
mountSessionDebugger,
|
|
10079
11353
|
newInterval,
|
|
10080
11354
|
parseCardHistoryID,
|
|
11355
|
+
pipelineDebugAPI,
|
|
10081
11356
|
processCustomQuestionsData,
|
|
11357
|
+
recordCardPresentation,
|
|
10082
11358
|
recordUserOutcome,
|
|
10083
11359
|
registerBlanksCard,
|
|
10084
11360
|
registerCustomQuestionTypes,
|
|
@@ -10091,6 +11367,9 @@ init_factory();
|
|
|
10091
11367
|
removeQuestionType,
|
|
10092
11368
|
runPeriodUpdate,
|
|
10093
11369
|
scoreAccuracyInZone,
|
|
11370
|
+
sessionDebugAPI,
|
|
11371
|
+
snapshotQueues,
|
|
11372
|
+
startSessionTracking,
|
|
10094
11373
|
updateLearningState,
|
|
10095
11374
|
updateStrategyWeight,
|
|
10096
11375
|
validateMigration,
|