@vue-skuilder/db 0.1.32-b → 0.1.32-e
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-DF1nUbPQ.d.cts → contentSource-BMlMwSiG.d.cts} +124 -5
- package/dist/{contentSource-Bdwkvqa8.d.ts → contentSource-Ht3N2f-y.d.ts} +124 -5
- package/dist/core/index.d.cts +26 -83
- package/dist/core/index.d.ts +26 -83
- package/dist/core/index.js +767 -71
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +767 -71
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BQdfJuBN.d.cts → dataLayerProvider-BEqB8VBR.d.cts} +1 -1
- package/dist/{dataLayerProvider-BKmVoyJR.d.ts → dataLayerProvider-DObSXjnf.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +18 -5
- package/dist/impl/couch/index.d.ts +18 -5
- package/dist/impl/couch/index.js +817 -74
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +817 -74
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +4 -4
- package/dist/impl/static/index.d.ts +4 -4
- package/dist/impl/static/index.js +763 -67
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +763 -67
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +23 -8
- package/dist/index.d.ts +23 -8
- package/dist/index.js +872 -86
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +872 -86
- package/dist/index.mjs.map +1 -1
- package/docs/navigators-architecture.md +2 -2
- package/package.json +2 -2
- package/src/core/interfaces/contentSource.ts +3 -3
- package/src/core/navigators/Pipeline.ts +104 -32
- package/src/core/navigators/PipelineDebugger.ts +152 -1
- package/src/core/navigators/filters/hierarchyDefinition.ts +90 -6
- package/src/core/navigators/filters/interferenceMitigator.ts +2 -1
- package/src/core/navigators/filters/relativePriority.ts +2 -1
- package/src/core/navigators/filters/userTagPreference.ts +2 -1
- package/src/core/navigators/generators/CompositeGenerator.ts +58 -5
- package/src/core/navigators/generators/elo.ts +7 -7
- package/src/core/navigators/generators/prescribed.ts +710 -46
- package/src/core/navigators/generators/srs.ts +3 -4
- package/src/core/navigators/generators/types.ts +48 -2
- package/src/core/navigators/index.ts +4 -3
- package/src/impl/couch/CourseSyncService.ts +72 -4
- package/src/impl/couch/classroomDB.ts +4 -3
- package/src/impl/couch/courseDB.ts +5 -4
- package/src/impl/static/courseDB.ts +5 -4
- package/src/study/SessionController.ts +58 -10
- package/src/study/TagFilteredContentSource.ts +4 -3
- package/src/study/services/EloService.ts +22 -3
- package/src/study/services/ResponseProcessor.ts +7 -3
package/dist/core/index.js
CHANGED
|
@@ -750,13 +750,20 @@ function captureRun(report) {
|
|
|
750
750
|
runHistory.pop();
|
|
751
751
|
}
|
|
752
752
|
}
|
|
753
|
-
function
|
|
753
|
+
function parseCardElo(provenance) {
|
|
754
|
+
const eloEntry = provenance.find((p) => p.strategy === "elo");
|
|
755
|
+
if (!eloEntry?.reason) return void 0;
|
|
756
|
+
const match = eloEntry.reason.match(/card:\s*(\d+)/);
|
|
757
|
+
return match ? parseInt(match[1], 10) : void 0;
|
|
758
|
+
}
|
|
759
|
+
function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
|
|
754
760
|
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
755
761
|
const cards = allCards.map((card) => ({
|
|
756
762
|
cardId: card.cardId,
|
|
757
763
|
courseId: card.courseId,
|
|
758
764
|
origin: getOrigin(card),
|
|
759
765
|
finalScore: card.score,
|
|
766
|
+
cardElo: parseCardElo(card.provenance),
|
|
760
767
|
provenance: card.provenance,
|
|
761
768
|
tags: card.tags,
|
|
762
769
|
selected: selectedIds.has(card.cardId)
|
|
@@ -766,6 +773,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
766
773
|
return {
|
|
767
774
|
courseId,
|
|
768
775
|
courseName,
|
|
776
|
+
userElo,
|
|
769
777
|
generatorName,
|
|
770
778
|
generators,
|
|
771
779
|
generatedCount,
|
|
@@ -786,6 +794,7 @@ function printRunSummary(run) {
|
|
|
786
794
|
console.group(`\u{1F50D} Pipeline Run: ${run.courseId} (${run.courseName || "unnamed"})`);
|
|
787
795
|
logger.info(`Run ID: ${run.runId}`);
|
|
788
796
|
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
797
|
+
logger.info(`User ELO: ${run.userElo ?? "unknown"}`);
|
|
789
798
|
logger.info(`Generator: ${run.generatorName} \u2192 ${run.generatedCount} candidates`);
|
|
790
799
|
if (run.generators && run.generators.length > 0) {
|
|
791
800
|
console.group("Generator breakdown:");
|
|
@@ -872,8 +881,12 @@ var init_PipelineDebugger = __esm({
|
|
|
872
881
|
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
873
882
|
logger.info(`Course: ${card.courseId}`);
|
|
874
883
|
logger.info(`Origin: ${card.origin}`);
|
|
884
|
+
logger.info(`Card ELO: ${card.cardElo ?? "unknown"} | User ELO: ${run.userElo ?? "unknown"}`);
|
|
875
885
|
logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
|
|
876
886
|
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
887
|
+
if (card.tags && card.tags.length > 0) {
|
|
888
|
+
logger.info(`Tags (${card.tags.length}): ${card.tags.join(", ")}`);
|
|
889
|
+
}
|
|
877
890
|
logger.info("Provenance:");
|
|
878
891
|
logger.info(formatProvenance(card.provenance));
|
|
879
892
|
console.groupEnd();
|
|
@@ -926,6 +939,81 @@ var init_PipelineDebugger = __esm({
|
|
|
926
939
|
}
|
|
927
940
|
console.groupEnd();
|
|
928
941
|
},
|
|
942
|
+
/**
|
|
943
|
+
* Show prescribed-related cards from the most recent run.
|
|
944
|
+
*
|
|
945
|
+
* Highlights:
|
|
946
|
+
* - cards directly generated by the prescribed strategy
|
|
947
|
+
* - blocked prescribed targets mentioned in provenance
|
|
948
|
+
* - support tags resolved for blocked targets
|
|
949
|
+
*
|
|
950
|
+
* @param groupId - Optional prescribed group ID filter (e.g. 'intro-core')
|
|
951
|
+
*/
|
|
952
|
+
showPrescribed(groupId) {
|
|
953
|
+
if (runHistory.length === 0) {
|
|
954
|
+
logger.info("[Pipeline Debug] No runs captured yet.");
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
const run = runHistory[0];
|
|
958
|
+
const prescribedCards = run.cards.filter(
|
|
959
|
+
(c) => c.provenance.some((p) => p.strategy === "prescribed")
|
|
960
|
+
);
|
|
961
|
+
console.group(`\u{1F9ED} Prescribed Debug (${run.courseId})`);
|
|
962
|
+
if (prescribedCards.length === 0) {
|
|
963
|
+
logger.info("No prescribed-generated cards were present in the most recent run.");
|
|
964
|
+
console.groupEnd();
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
const rows = prescribedCards.map((card) => {
|
|
968
|
+
const prescribedProv = card.provenance.find((p) => p.strategy === "prescribed");
|
|
969
|
+
const reason = prescribedProv?.reason ?? "";
|
|
970
|
+
const parsedGroup = reason.match(/group=([^;]+)/)?.[1] ?? "unknown";
|
|
971
|
+
const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
|
|
972
|
+
const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
|
|
973
|
+
const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
|
|
974
|
+
const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
|
|
975
|
+
const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
|
|
976
|
+
return {
|
|
977
|
+
group: parsedGroup,
|
|
978
|
+
mode,
|
|
979
|
+
cardId: card.cardId,
|
|
980
|
+
selected: card.selected ? "yes" : "no",
|
|
981
|
+
finalScore: card.finalScore.toFixed(3),
|
|
982
|
+
blocked,
|
|
983
|
+
blockedTargets,
|
|
984
|
+
supportTags,
|
|
985
|
+
multiplier
|
|
986
|
+
};
|
|
987
|
+
}).filter((row) => !groupId || row.group === groupId).sort((a, b) => Number(b.finalScore) - Number(a.finalScore));
|
|
988
|
+
if (rows.length === 0) {
|
|
989
|
+
logger.info(
|
|
990
|
+
`[Pipeline Debug] No prescribed cards matched group '${groupId}' in the most recent run.`
|
|
991
|
+
);
|
|
992
|
+
console.groupEnd();
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
console.table(rows);
|
|
996
|
+
const selectedRows = rows.filter((r) => r.selected === "yes");
|
|
997
|
+
const blockedTargetSet = /* @__PURE__ */ new Set();
|
|
998
|
+
const supportTagSet = /* @__PURE__ */ new Set();
|
|
999
|
+
for (const row of rows) {
|
|
1000
|
+
if (row.blockedTargets && row.blockedTargets !== "none") {
|
|
1001
|
+
row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
|
|
1002
|
+
}
|
|
1003
|
+
if (row.supportTags && row.supportTags !== "none") {
|
|
1004
|
+
row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
logger.info(`Prescribed cards in run: ${rows.length}`);
|
|
1008
|
+
logger.info(`Selected prescribed cards: ${selectedRows.length}`);
|
|
1009
|
+
logger.info(
|
|
1010
|
+
`Blocked prescribed targets referenced: ${blockedTargetSet.size > 0 ? [...blockedTargetSet].join(", ") : "none"}`
|
|
1011
|
+
);
|
|
1012
|
+
logger.info(
|
|
1013
|
+
`Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
|
|
1014
|
+
);
|
|
1015
|
+
console.groupEnd();
|
|
1016
|
+
},
|
|
929
1017
|
/**
|
|
930
1018
|
* Show all runs in compact format.
|
|
931
1019
|
*/
|
|
@@ -1037,6 +1125,27 @@ var init_PipelineDebugger = __esm({
|
|
|
1037
1125
|
}
|
|
1038
1126
|
return _activePipeline.diagnoseCardSpace({ threshold });
|
|
1039
1127
|
},
|
|
1128
|
+
/**
|
|
1129
|
+
* Show user's per-tag ELO data. Useful for diagnosing hierarchy gate status.
|
|
1130
|
+
*
|
|
1131
|
+
* @param tagFilter - Optional glob pattern(s) to filter tags.
|
|
1132
|
+
* Examples: 'gpc:expose:*', 'gpc:intro:t-T', ['gpc:expose:t-*', 'gpc:intro:t-*']
|
|
1133
|
+
*/
|
|
1134
|
+
async showTagElo(tagFilter) {
|
|
1135
|
+
if (!_activePipeline) {
|
|
1136
|
+
logger.info("[Pipeline Debug] No active pipeline. Run a session first.");
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const status = await _activePipeline.getTagEloStatus(tagFilter);
|
|
1140
|
+
const entries = Object.entries(status).sort(([a], [b]) => a.localeCompare(b));
|
|
1141
|
+
if (entries.length === 0) {
|
|
1142
|
+
logger.info(`[Pipeline Debug] No tag ELO data found${tagFilter ? ` for pattern: ${tagFilter}` : ""}.`);
|
|
1143
|
+
return;
|
|
1144
|
+
}
|
|
1145
|
+
console.table(
|
|
1146
|
+
Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
|
|
1147
|
+
);
|
|
1148
|
+
},
|
|
1040
1149
|
/**
|
|
1041
1150
|
* Show help.
|
|
1042
1151
|
*/
|
|
@@ -1048,10 +1157,12 @@ Commands:
|
|
|
1048
1157
|
.showLastRun() Show summary of most recent pipeline run
|
|
1049
1158
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
1050
1159
|
.showCard(cardId) Show provenance trail for a specific card
|
|
1160
|
+
.showTagElo(pattern) Show user's tag ELO data (async). E.g. 'gpc:expose:*'
|
|
1051
1161
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
1052
1162
|
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
1053
1163
|
.showRegistry() Show navigator registry (classes + roles)
|
|
1054
1164
|
.showStrategies() Show registry + strategy mapping from last run
|
|
1165
|
+
.showPrescribed(id?) Show prescribed-generated cards and blocked/support details from last run
|
|
1055
1166
|
.listRuns() List all captured runs in table format
|
|
1056
1167
|
.export() Export run history as JSON for bug reports
|
|
1057
1168
|
.clear() Clear run history
|
|
@@ -1075,6 +1186,44 @@ __export(CompositeGenerator_exports, {
|
|
|
1075
1186
|
AggregationMode: () => AggregationMode,
|
|
1076
1187
|
default: () => CompositeGenerator
|
|
1077
1188
|
});
|
|
1189
|
+
function mergeHints(allHints) {
|
|
1190
|
+
const defined = allHints.filter((h) => h !== void 0);
|
|
1191
|
+
if (defined.length === 0) return void 0;
|
|
1192
|
+
const merged = {};
|
|
1193
|
+
const boostTags = {};
|
|
1194
|
+
for (const hints of defined) {
|
|
1195
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
|
|
1196
|
+
boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
if (Object.keys(boostTags).length > 0) {
|
|
1200
|
+
merged.boostTags = boostTags;
|
|
1201
|
+
}
|
|
1202
|
+
const boostCards = {};
|
|
1203
|
+
for (const hints of defined) {
|
|
1204
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
|
|
1205
|
+
boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
if (Object.keys(boostCards).length > 0) {
|
|
1209
|
+
merged.boostCards = boostCards;
|
|
1210
|
+
}
|
|
1211
|
+
const concatUnique = (field) => {
|
|
1212
|
+
const values = defined.flatMap((h) => h[field] ?? []);
|
|
1213
|
+
if (values.length > 0) {
|
|
1214
|
+
merged[field] = [...new Set(values)];
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
concatUnique("requireTags");
|
|
1218
|
+
concatUnique("requireCards");
|
|
1219
|
+
concatUnique("excludeTags");
|
|
1220
|
+
concatUnique("excludeCards");
|
|
1221
|
+
const labels = defined.map((h) => h._label).filter(Boolean);
|
|
1222
|
+
if (labels.length > 0) {
|
|
1223
|
+
merged._label = labels.join("; ");
|
|
1224
|
+
}
|
|
1225
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
1226
|
+
}
|
|
1078
1227
|
var AggregationMode, DEFAULT_AGGREGATION_MODE, FREQUENCY_BOOST_FACTOR, CompositeGenerator;
|
|
1079
1228
|
var init_CompositeGenerator = __esm({
|
|
1080
1229
|
"src/core/navigators/generators/CompositeGenerator.ts"() {
|
|
@@ -1138,17 +1287,18 @@ var init_CompositeGenerator = __esm({
|
|
|
1138
1287
|
this.generators.map((g) => g.getWeightedCards(limit, context))
|
|
1139
1288
|
);
|
|
1140
1289
|
const generatorSummaries = [];
|
|
1141
|
-
results.forEach((
|
|
1290
|
+
results.forEach((result, index) => {
|
|
1291
|
+
const cards2 = result.cards;
|
|
1142
1292
|
const gen = this.generators[index];
|
|
1143
1293
|
const genName = gen.name || `Generator ${index}`;
|
|
1144
|
-
const newCards =
|
|
1145
|
-
const reviewCards =
|
|
1146
|
-
if (
|
|
1147
|
-
const topScore = Math.max(...
|
|
1294
|
+
const newCards = cards2.filter((c) => c.provenance[0]?.reason?.includes("new card"));
|
|
1295
|
+
const reviewCards = cards2.filter((c) => c.provenance[0]?.reason?.includes("review"));
|
|
1296
|
+
if (cards2.length > 0) {
|
|
1297
|
+
const topScore = Math.max(...cards2.map((c) => c.score)).toFixed(2);
|
|
1148
1298
|
const parts = [];
|
|
1149
1299
|
if (newCards.length > 0) parts.push(`${newCards.length} new`);
|
|
1150
1300
|
if (reviewCards.length > 0) parts.push(`${reviewCards.length} reviews`);
|
|
1151
|
-
const breakdown = parts.length > 0 ? parts.join(", ") : `${
|
|
1301
|
+
const breakdown = parts.length > 0 ? parts.join(", ") : `${cards2.length} cards`;
|
|
1152
1302
|
generatorSummaries.push(`${genName}: ${breakdown} (top: ${topScore})`);
|
|
1153
1303
|
} else {
|
|
1154
1304
|
generatorSummaries.push(`${genName}: 0 cards`);
|
|
@@ -1156,7 +1306,8 @@ var init_CompositeGenerator = __esm({
|
|
|
1156
1306
|
});
|
|
1157
1307
|
logger.info(`[Composite] Generator breakdown: ${generatorSummaries.join(" | ")}`);
|
|
1158
1308
|
const byCardId = /* @__PURE__ */ new Map();
|
|
1159
|
-
results.forEach((
|
|
1309
|
+
results.forEach((result, index) => {
|
|
1310
|
+
const cards2 = result.cards;
|
|
1160
1311
|
const gen = this.generators[index];
|
|
1161
1312
|
let weight = gen.learnable?.weight ?? 1;
|
|
1162
1313
|
let deviation;
|
|
@@ -1167,7 +1318,7 @@ var init_CompositeGenerator = __esm({
|
|
|
1167
1318
|
deviation = context.orchestration.getDeviation(strategyId);
|
|
1168
1319
|
}
|
|
1169
1320
|
}
|
|
1170
|
-
for (const card of
|
|
1321
|
+
for (const card of cards2) {
|
|
1171
1322
|
if (card.provenance.length > 0) {
|
|
1172
1323
|
card.provenance[0].effectiveWeight = weight;
|
|
1173
1324
|
card.provenance[0].deviation = deviation;
|
|
@@ -1179,15 +1330,15 @@ var init_CompositeGenerator = __esm({
|
|
|
1179
1330
|
});
|
|
1180
1331
|
const merged = [];
|
|
1181
1332
|
for (const [, items] of byCardId) {
|
|
1182
|
-
const
|
|
1333
|
+
const cards2 = items.map((i) => i.card);
|
|
1183
1334
|
const aggregatedScore = this.aggregateScores(items);
|
|
1184
1335
|
const finalScore = Math.min(1, aggregatedScore);
|
|
1185
|
-
const mergedProvenance =
|
|
1186
|
-
const initialScore =
|
|
1336
|
+
const mergedProvenance = cards2.flatMap((c) => c.provenance);
|
|
1337
|
+
const initialScore = cards2[0].score;
|
|
1187
1338
|
const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
|
|
1188
1339
|
const reason = this.buildAggregationReason(items, finalScore);
|
|
1189
1340
|
merged.push({
|
|
1190
|
-
...
|
|
1341
|
+
...cards2[0],
|
|
1191
1342
|
score: finalScore,
|
|
1192
1343
|
provenance: [
|
|
1193
1344
|
...mergedProvenance,
|
|
@@ -1202,7 +1353,9 @@ var init_CompositeGenerator = __esm({
|
|
|
1202
1353
|
]
|
|
1203
1354
|
});
|
|
1204
1355
|
}
|
|
1205
|
-
|
|
1356
|
+
const cards = merged.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
1357
|
+
const hints = mergeHints(results.map((result) => result.hints));
|
|
1358
|
+
return { cards, hints };
|
|
1206
1359
|
}
|
|
1207
1360
|
/**
|
|
1208
1361
|
* Build human-readable reason for score aggregation.
|
|
@@ -1333,16 +1486,16 @@ var init_elo = __esm({
|
|
|
1333
1486
|
};
|
|
1334
1487
|
});
|
|
1335
1488
|
scored.sort((a, b) => b.score - a.score);
|
|
1336
|
-
const
|
|
1337
|
-
if (
|
|
1338
|
-
const topScores =
|
|
1489
|
+
const cards = scored.slice(0, limit);
|
|
1490
|
+
if (cards.length > 0) {
|
|
1491
|
+
const topScores = cards.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ");
|
|
1339
1492
|
logger.info(
|
|
1340
|
-
`[ELO] Course ${this.course.getCourseID()}: ${
|
|
1493
|
+
`[ELO] Course ${this.course.getCourseID()}: ${cards.length} new cards (top scores: ${topScores})`
|
|
1341
1494
|
);
|
|
1342
1495
|
} else {
|
|
1343
1496
|
logger.info(`[ELO] Course ${this.course.getCourseID()}: No new cards available`);
|
|
1344
1497
|
}
|
|
1345
|
-
return
|
|
1498
|
+
return { cards };
|
|
1346
1499
|
}
|
|
1347
1500
|
};
|
|
1348
1501
|
}
|
|
@@ -1361,60 +1514,476 @@ var prescribed_exports = {};
|
|
|
1361
1514
|
__export(prescribed_exports, {
|
|
1362
1515
|
default: () => PrescribedCardsGenerator
|
|
1363
1516
|
});
|
|
1364
|
-
|
|
1517
|
+
function dedupe(arr) {
|
|
1518
|
+
return [...new Set(arr)];
|
|
1519
|
+
}
|
|
1520
|
+
function isoNow() {
|
|
1521
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1522
|
+
}
|
|
1523
|
+
function clamp(value, min, max) {
|
|
1524
|
+
return Math.max(min, Math.min(max, value));
|
|
1525
|
+
}
|
|
1526
|
+
function matchesTagPattern(tag, pattern) {
|
|
1527
|
+
if (pattern === "*") return true;
|
|
1528
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1529
|
+
const re = new RegExp(`^${escaped}$`);
|
|
1530
|
+
return re.test(tag);
|
|
1531
|
+
}
|
|
1532
|
+
function pickTopByScore(cards, limit) {
|
|
1533
|
+
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1534
|
+
}
|
|
1535
|
+
var DEFAULT_FRESHNESS_WINDOW, DEFAULT_MAX_DIRECT_PER_RUN, DEFAULT_MAX_SUPPORT_PER_RUN, DEFAULT_HIERARCHY_DEPTH, DEFAULT_MIN_COUNT, BASE_TARGET_SCORE, BASE_SUPPORT_SCORE, MAX_TARGET_MULTIPLIER, MAX_SUPPORT_MULTIPLIER, LOCKED_TAG_PREFIXES, LESSON_GATE_PENALTY_TAG_HINT, PRESCRIBED_DEBUG_VERSION, PrescribedCardsGenerator;
|
|
1365
1536
|
var init_prescribed = __esm({
|
|
1366
1537
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1367
1538
|
"use strict";
|
|
1368
1539
|
init_navigators();
|
|
1369
1540
|
init_logger();
|
|
1541
|
+
DEFAULT_FRESHNESS_WINDOW = 3;
|
|
1542
|
+
DEFAULT_MAX_DIRECT_PER_RUN = 3;
|
|
1543
|
+
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1544
|
+
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1545
|
+
DEFAULT_MIN_COUNT = 3;
|
|
1546
|
+
BASE_TARGET_SCORE = 1;
|
|
1547
|
+
BASE_SUPPORT_SCORE = 0.8;
|
|
1548
|
+
MAX_TARGET_MULTIPLIER = 8;
|
|
1549
|
+
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1550
|
+
LOCKED_TAG_PREFIXES = ["concept:"];
|
|
1551
|
+
LESSON_GATE_PENALTY_TAG_HINT = "concept:";
|
|
1552
|
+
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
|
|
1370
1553
|
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1371
1554
|
name;
|
|
1372
1555
|
config;
|
|
1373
1556
|
constructor(user, course, strategyData) {
|
|
1374
1557
|
super(user, course, strategyData);
|
|
1375
1558
|
this.name = strategyData.name || "Prescribed Cards";
|
|
1376
|
-
|
|
1377
|
-
const parsed = JSON.parse(strategyData.serializedData);
|
|
1378
|
-
this.config = { cardIds: parsed.cardIds || [] };
|
|
1379
|
-
} catch {
|
|
1380
|
-
this.config = { cardIds: [] };
|
|
1381
|
-
}
|
|
1559
|
+
this.config = this.parseConfig(strategyData.serializedData);
|
|
1382
1560
|
logger.debug(
|
|
1383
|
-
`[Prescribed] Initialized with ${this.config.
|
|
1561
|
+
`[Prescribed] Initialized with ${this.config.groups.length} groups and ${this.config.groups.reduce((n, g) => n + g.targetCardIds.length, 0)} targets`
|
|
1384
1562
|
);
|
|
1385
1563
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1564
|
+
get strategyKey() {
|
|
1565
|
+
return "PrescribedProgress";
|
|
1566
|
+
}
|
|
1567
|
+
async getWeightedCards(limit, context) {
|
|
1568
|
+
if (this.config.groups.length === 0 || limit <= 0) {
|
|
1569
|
+
return { cards: [] };
|
|
1389
1570
|
}
|
|
1390
1571
|
const courseId = this.course.getCourseID();
|
|
1391
1572
|
const activeCards = await this.user.getActiveCards();
|
|
1392
1573
|
const activeIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1574
|
+
const seenCards = await this.user.getSeenCards(courseId).catch(() => []);
|
|
1575
|
+
const seenIds = new Set(seenCards);
|
|
1576
|
+
const progress = await this.getStrategyState() ?? {
|
|
1577
|
+
updatedAt: isoNow(),
|
|
1578
|
+
groups: {}
|
|
1579
|
+
};
|
|
1580
|
+
const hierarchyConfigs = await this.loadHierarchyConfigs();
|
|
1581
|
+
const courseReg = await this.user.getCourseRegDoc(courseId).catch(() => null);
|
|
1582
|
+
const userGlobalElo = typeof courseReg?.elo === "number" ? courseReg.elo : courseReg?.elo?.global?.score ?? context?.userElo ?? 1e3;
|
|
1583
|
+
const userTagElo = typeof courseReg?.elo === "number" ? {} : courseReg?.elo?.tags ?? {};
|
|
1584
|
+
const allTargetIds = dedupe(this.config.groups.flatMap((g) => g.targetCardIds));
|
|
1585
|
+
const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
|
|
1586
|
+
const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
|
|
1587
|
+
const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
|
|
1588
|
+
const nextState = {
|
|
1589
|
+
updatedAt: isoNow(),
|
|
1590
|
+
groups: {}
|
|
1591
|
+
};
|
|
1592
|
+
const emitted = [];
|
|
1593
|
+
const emittedIds = /* @__PURE__ */ new Set();
|
|
1594
|
+
const groupRuntimes = [];
|
|
1595
|
+
for (const group of this.config.groups) {
|
|
1596
|
+
const runtime = this.buildGroupRuntimeState({
|
|
1597
|
+
group,
|
|
1598
|
+
priorState: progress.groups[group.id],
|
|
1599
|
+
activeIds,
|
|
1600
|
+
seenIds,
|
|
1601
|
+
tagsByCard,
|
|
1602
|
+
hierarchyConfigs,
|
|
1603
|
+
userTagElo,
|
|
1604
|
+
userGlobalElo
|
|
1605
|
+
});
|
|
1606
|
+
groupRuntimes.push(runtime);
|
|
1607
|
+
nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
|
|
1608
|
+
const directCards = this.buildDirectTargetCards(
|
|
1609
|
+
runtime,
|
|
1610
|
+
courseId,
|
|
1611
|
+
emittedIds
|
|
1612
|
+
);
|
|
1613
|
+
const supportCards = this.buildSupportCards(
|
|
1614
|
+
runtime,
|
|
1615
|
+
courseId,
|
|
1616
|
+
emittedIds
|
|
1617
|
+
);
|
|
1618
|
+
emitted.push(...directCards, ...supportCards);
|
|
1619
|
+
}
|
|
1620
|
+
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
1621
|
+
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
1622
|
+
boostTags: hintSummary.boostTags,
|
|
1623
|
+
_label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
|
|
1624
|
+
} : void 0;
|
|
1625
|
+
if (emitted.length === 0) {
|
|
1626
|
+
logger.debug("[Prescribed] No prescribed targets/support emitted this run");
|
|
1627
|
+
await this.putStrategyState(nextState).catch((e) => {
|
|
1628
|
+
logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
|
|
1629
|
+
});
|
|
1630
|
+
return hints ? { cards: [], hints } : { cards: [] };
|
|
1631
|
+
}
|
|
1632
|
+
const finalCards = pickTopByScore(emitted, limit);
|
|
1633
|
+
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
1634
|
+
for (const card of finalCards) {
|
|
1635
|
+
const prov = card.provenance[0];
|
|
1636
|
+
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
1637
|
+
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
1638
|
+
if (!groupId) continue;
|
|
1639
|
+
if (!surfacedByGroup.has(groupId)) {
|
|
1640
|
+
surfacedByGroup.set(groupId, { targetIds: [], supportIds: [] });
|
|
1641
|
+
}
|
|
1642
|
+
surfacedByGroup.get(groupId)[mode].push(card.cardId);
|
|
1643
|
+
}
|
|
1644
|
+
for (const group of this.config.groups) {
|
|
1645
|
+
const groupState = nextState.groups[group.id];
|
|
1646
|
+
const surfaced = surfacedByGroup.get(group.id);
|
|
1647
|
+
if (surfaced && (surfaced.targetIds.length > 0 || surfaced.supportIds.length > 0)) {
|
|
1648
|
+
groupState.lastSurfacedAt = isoNow();
|
|
1649
|
+
groupState.sessionsSinceSurfaced = 0;
|
|
1650
|
+
if (surfaced.supportIds.length > 0) {
|
|
1651
|
+
groupState.lastSupportAt = isoNow();
|
|
1410
1652
|
}
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
await this.putStrategyState(nextState).catch((e) => {
|
|
1656
|
+
logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
|
|
1657
|
+
});
|
|
1413
1658
|
logger.info(
|
|
1414
|
-
`[Prescribed] Emitting ${
|
|
1659
|
+
`[Prescribed] Emitting ${finalCards.length} cards (${finalCards.filter((c) => c.provenance[0]?.reason.includes("mode=target")).length} target, ${finalCards.filter((c) => c.provenance[0]?.reason.includes("mode=support")).length} support)`
|
|
1415
1660
|
);
|
|
1661
|
+
return hints ? { cards: finalCards, hints } : { cards: finalCards };
|
|
1662
|
+
}
|
|
1663
|
+
buildSupportHintSummary(groupRuntimes) {
|
|
1664
|
+
const boostTags = {};
|
|
1665
|
+
const blockedTargetIds = /* @__PURE__ */ new Set();
|
|
1666
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1667
|
+
for (const runtime of groupRuntimes) {
|
|
1668
|
+
if (runtime.blockedTargets.length === 0 || runtime.supportTags.length === 0) {
|
|
1669
|
+
continue;
|
|
1670
|
+
}
|
|
1671
|
+
runtime.blockedTargets.forEach((cardId) => blockedTargetIds.add(cardId));
|
|
1672
|
+
for (const tag of runtime.supportTags) {
|
|
1673
|
+
supportTags.add(tag);
|
|
1674
|
+
boostTags[tag] = (boostTags[tag] ?? 1) * runtime.supportMultiplier;
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
return {
|
|
1678
|
+
boostTags,
|
|
1679
|
+
blockedTargetIds: [...blockedTargetIds].sort(),
|
|
1680
|
+
supportTags: [...supportTags].sort()
|
|
1681
|
+
};
|
|
1682
|
+
}
|
|
1683
|
+
parseConfig(serializedData) {
|
|
1684
|
+
try {
|
|
1685
|
+
const parsed = JSON.parse(serializedData);
|
|
1686
|
+
const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
|
|
1687
|
+
const groups = groupsRaw.map((raw, i) => ({
|
|
1688
|
+
id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
|
|
1689
|
+
targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
|
|
1690
|
+
supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
|
|
1691
|
+
supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
|
|
1692
|
+
freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
|
|
1693
|
+
maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
|
|
1694
|
+
maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
|
|
1695
|
+
hierarchyWalk: {
|
|
1696
|
+
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
1697
|
+
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
1698
|
+
},
|
|
1699
|
+
retireOnEncounter: raw.retireOnEncounter !== false
|
|
1700
|
+
})).filter((g) => g.targetCardIds.length > 0);
|
|
1701
|
+
return { groups };
|
|
1702
|
+
} catch {
|
|
1703
|
+
return { groups: [] };
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
async loadHierarchyConfigs() {
|
|
1707
|
+
try {
|
|
1708
|
+
const strategies = await this.course.getNavigationStrategies();
|
|
1709
|
+
return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
|
|
1710
|
+
try {
|
|
1711
|
+
const parsed = JSON.parse(s.serializedData);
|
|
1712
|
+
return {
|
|
1713
|
+
prerequisites: parsed.prerequisites || {}
|
|
1714
|
+
};
|
|
1715
|
+
} catch {
|
|
1716
|
+
return { prerequisites: {} };
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
} catch (e) {
|
|
1720
|
+
logger.debug(`[Prescribed] Failed to load hierarchy configs: ${e}`);
|
|
1721
|
+
return [];
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
buildGroupRuntimeState(args) {
|
|
1725
|
+
const {
|
|
1726
|
+
group,
|
|
1727
|
+
priorState,
|
|
1728
|
+
activeIds,
|
|
1729
|
+
seenIds,
|
|
1730
|
+
tagsByCard,
|
|
1731
|
+
hierarchyConfigs,
|
|
1732
|
+
userTagElo,
|
|
1733
|
+
userGlobalElo
|
|
1734
|
+
} = args;
|
|
1735
|
+
const encounteredTargets = /* @__PURE__ */ new Set();
|
|
1736
|
+
for (const cardId of group.targetCardIds) {
|
|
1737
|
+
if (activeIds.has(cardId) || seenIds.has(cardId)) {
|
|
1738
|
+
encounteredTargets.add(cardId);
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
if (priorState?.encounteredCardIds?.length) {
|
|
1742
|
+
for (const cardId of priorState.encounteredCardIds) {
|
|
1743
|
+
encounteredTargets.add(cardId);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
const pendingTargets = group.targetCardIds.filter((id) => !encounteredTargets.has(id));
|
|
1747
|
+
const targetTags = /* @__PURE__ */ new Map();
|
|
1748
|
+
for (const cardId of pendingTargets) {
|
|
1749
|
+
targetTags.set(cardId, tagsByCard.get(cardId) ?? []);
|
|
1750
|
+
}
|
|
1751
|
+
const blockedTargets = [];
|
|
1752
|
+
const surfaceableTargets = [];
|
|
1753
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1754
|
+
for (const cardId of pendingTargets) {
|
|
1755
|
+
const tags = targetTags.get(cardId) ?? [];
|
|
1756
|
+
const resolution = this.resolveBlockedSupportTags(
|
|
1757
|
+
tags,
|
|
1758
|
+
hierarchyConfigs,
|
|
1759
|
+
userTagElo,
|
|
1760
|
+
userGlobalElo,
|
|
1761
|
+
group.hierarchyWalk?.enabled !== false,
|
|
1762
|
+
group.hierarchyWalk?.maxDepth ?? DEFAULT_HIERARCHY_DEPTH
|
|
1763
|
+
);
|
|
1764
|
+
const introTags = tags.filter((tag) => tag.startsWith("gpc:intro:"));
|
|
1765
|
+
const exposeTags = new Set(tags.filter((tag) => tag.startsWith("gpc:expose:")));
|
|
1766
|
+
for (const introTag of introTags) {
|
|
1767
|
+
const suffix = introTag.slice("gpc:intro:".length);
|
|
1768
|
+
if (suffix) {
|
|
1769
|
+
exposeTags.add(`gpc:expose:${suffix}`);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
const unmetExposeTags = [...exposeTags].filter((tag) => {
|
|
1773
|
+
const tagElo = userTagElo[tag];
|
|
1774
|
+
return !tagElo || tagElo.count < DEFAULT_MIN_COUNT;
|
|
1775
|
+
});
|
|
1776
|
+
if (unmetExposeTags.length > 0) {
|
|
1777
|
+
unmetExposeTags.forEach((tag) => supportTags.add(tag));
|
|
1778
|
+
}
|
|
1779
|
+
if (resolution.blocked || unmetExposeTags.length > 0) {
|
|
1780
|
+
blockedTargets.push(cardId);
|
|
1781
|
+
resolution.supportTags.forEach((t) => supportTags.add(t));
|
|
1782
|
+
} else {
|
|
1783
|
+
surfaceableTargets.push(cardId);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
const supportCandidates = dedupe([
|
|
1787
|
+
...group.supportCardIds ?? [],
|
|
1788
|
+
...this.findSupportCardsByTags(
|
|
1789
|
+
group,
|
|
1790
|
+
tagsByCard,
|
|
1791
|
+
[...supportTags]
|
|
1792
|
+
)
|
|
1793
|
+
]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
|
|
1794
|
+
const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
|
|
1795
|
+
const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
1796
|
+
const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
|
|
1797
|
+
const pressureMultiplier = pendingTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.75 + Math.min(2, pendingTargets.length * 0.1), 1, MAX_TARGET_MULTIPLIER);
|
|
1798
|
+
const supportMultiplier = blockedTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.5 + Math.min(1.5, blockedTargets.length * 0.15), 1, MAX_SUPPORT_MULTIPLIER);
|
|
1799
|
+
return {
|
|
1800
|
+
group,
|
|
1801
|
+
encounteredTargets,
|
|
1802
|
+
pendingTargets,
|
|
1803
|
+
blockedTargets,
|
|
1804
|
+
surfaceableTargets,
|
|
1805
|
+
targetTags,
|
|
1806
|
+
supportCandidates,
|
|
1807
|
+
supportTags: [...supportTags],
|
|
1808
|
+
pressureMultiplier,
|
|
1809
|
+
supportMultiplier,
|
|
1810
|
+
debugVersion: PRESCRIBED_DEBUG_VERSION
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
buildNextGroupState(runtime, prior) {
|
|
1814
|
+
const carriedSessions = prior?.sessionsSinceSurfaced ?? 0;
|
|
1815
|
+
const surfacedThisRun = false;
|
|
1816
|
+
return {
|
|
1817
|
+
encounteredCardIds: [...runtime.encounteredTargets].sort(),
|
|
1818
|
+
pendingTargetIds: [...runtime.pendingTargets].sort(),
|
|
1819
|
+
lastSurfacedAt: prior?.lastSurfacedAt ?? null,
|
|
1820
|
+
sessionsSinceSurfaced: surfacedThisRun ? 0 : carriedSessions + 1,
|
|
1821
|
+
lastSupportAt: prior?.lastSupportAt ?? null,
|
|
1822
|
+
blockedTargetIds: [...runtime.blockedTargets].sort(),
|
|
1823
|
+
lastResolvedSupportTags: [...runtime.supportTags].sort()
|
|
1824
|
+
};
|
|
1825
|
+
}
|
|
1826
|
+
buildDirectTargetCards(runtime, courseId, emittedIds) {
|
|
1827
|
+
const maxDirect = runtime.group.maxDirectTargetsPerRun ?? DEFAULT_MAX_DIRECT_PER_RUN;
|
|
1828
|
+
const directIds = runtime.surfaceableTargets.filter((id) => !emittedIds.has(id)).slice(0, maxDirect);
|
|
1829
|
+
const cards = [];
|
|
1830
|
+
for (const cardId of directIds) {
|
|
1831
|
+
emittedIds.add(cardId);
|
|
1832
|
+
cards.push({
|
|
1833
|
+
cardId,
|
|
1834
|
+
courseId,
|
|
1835
|
+
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1836
|
+
provenance: [
|
|
1837
|
+
{
|
|
1838
|
+
strategy: "prescribed",
|
|
1839
|
+
strategyName: this.strategyName || this.name,
|
|
1840
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1841
|
+
action: "generated",
|
|
1842
|
+
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1843
|
+
reason: `mode=target;group=${runtime.group.id};pending=${runtime.pendingTargets.length};surfaceable=${runtime.surfaceableTargets.length};blocked=${runtime.blockedTargets.length};blockedTargets=${runtime.blockedTargets.join("|") || "none"};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.pressureMultiplier.toFixed(2)};testversion=${runtime.debugVersion}`
|
|
1844
|
+
}
|
|
1845
|
+
]
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1416
1848
|
return cards;
|
|
1417
1849
|
}
|
|
1850
|
+
buildSupportCards(runtime, courseId, emittedIds) {
|
|
1851
|
+
if (runtime.blockedTargets.length === 0 || runtime.supportCandidates.length === 0) {
|
|
1852
|
+
return [];
|
|
1853
|
+
}
|
|
1854
|
+
const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
|
|
1855
|
+
const supportIds = runtime.supportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
|
|
1856
|
+
const cards = [];
|
|
1857
|
+
for (const cardId of supportIds) {
|
|
1858
|
+
emittedIds.add(cardId);
|
|
1859
|
+
cards.push({
|
|
1860
|
+
cardId,
|
|
1861
|
+
courseId,
|
|
1862
|
+
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1863
|
+
provenance: [
|
|
1864
|
+
{
|
|
1865
|
+
strategy: "prescribed",
|
|
1866
|
+
strategyName: this.strategyName || this.name,
|
|
1867
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1868
|
+
action: "generated",
|
|
1869
|
+
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1870
|
+
reason: `mode=support;group=${runtime.group.id};pending=${runtime.pendingTargets.length};blocked=${runtime.blockedTargets.length};blockedTargets=${runtime.blockedTargets.join("|") || "none"};supportCard=${cardId};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.supportMultiplier.toFixed(2)};testversion=${runtime.debugVersion}`
|
|
1871
|
+
}
|
|
1872
|
+
]
|
|
1873
|
+
});
|
|
1874
|
+
}
|
|
1875
|
+
return cards;
|
|
1876
|
+
}
|
|
1877
|
+
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
1878
|
+
if (supportTags.length === 0) {
|
|
1879
|
+
return [];
|
|
1880
|
+
}
|
|
1881
|
+
const explicitSupportIds = group.supportCardIds ?? [];
|
|
1882
|
+
const explicitPatterns = group.supportTagPatterns ?? [];
|
|
1883
|
+
if (explicitSupportIds.length === 0 && explicitPatterns.length === 0) {
|
|
1884
|
+
return [];
|
|
1885
|
+
}
|
|
1886
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1887
|
+
for (const cardId of explicitSupportIds) {
|
|
1888
|
+
const cardTags = tagsByCard.get(cardId) ?? [];
|
|
1889
|
+
const matchesResolved = supportTags.some((supportTag) => cardTags.includes(supportTag));
|
|
1890
|
+
const matchesPattern = explicitPatterns.some(
|
|
1891
|
+
(pattern) => cardTags.some((tag) => matchesTagPattern(tag, pattern))
|
|
1892
|
+
);
|
|
1893
|
+
if (matchesResolved || matchesPattern) {
|
|
1894
|
+
candidates.add(cardId);
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
return [...candidates];
|
|
1898
|
+
}
|
|
1899
|
+
resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
|
|
1900
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1901
|
+
let blocked = false;
|
|
1902
|
+
for (const targetTag of targetTags) {
|
|
1903
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[targetTag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
1904
|
+
if (prereqSets.length === 0) {
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
const tagBlocked = prereqSets.some(
|
|
1908
|
+
(prereqs) => prereqs.some((pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
1909
|
+
);
|
|
1910
|
+
if (!tagBlocked) {
|
|
1911
|
+
continue;
|
|
1912
|
+
}
|
|
1913
|
+
blocked = true;
|
|
1914
|
+
if (!hierarchyWalkEnabled) {
|
|
1915
|
+
for (const prereqs of prereqSets) {
|
|
1916
|
+
for (const prereq of prereqs) {
|
|
1917
|
+
if (!this.isPrerequisiteMet(prereq, userTagElo[prereq.tag], userGlobalElo)) {
|
|
1918
|
+
supportTags.add(prereq.tag);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
for (const prereqs of prereqSets) {
|
|
1925
|
+
for (const prereq of prereqs) {
|
|
1926
|
+
if (!this.isPrerequisiteMet(prereq, userTagElo[prereq.tag], userGlobalElo)) {
|
|
1927
|
+
this.collectSupportTagsRecursive(
|
|
1928
|
+
prereq.tag,
|
|
1929
|
+
hierarchyConfigs,
|
|
1930
|
+
userTagElo,
|
|
1931
|
+
userGlobalElo,
|
|
1932
|
+
maxDepth,
|
|
1933
|
+
/* @__PURE__ */ new Set(),
|
|
1934
|
+
supportTags
|
|
1935
|
+
);
|
|
1936
|
+
}
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
return { blocked, supportTags: [...supportTags] };
|
|
1941
|
+
}
|
|
1942
|
+
collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
|
|
1943
|
+
if (depth < 0 || visited.has(tag)) return;
|
|
1944
|
+
if (this.isHardGatedTag(tag)) return;
|
|
1945
|
+
visited.add(tag);
|
|
1946
|
+
let walkedFurther = false;
|
|
1947
|
+
for (const hierarchy of hierarchyConfigs) {
|
|
1948
|
+
const prereqs = hierarchy.prerequisites[tag];
|
|
1949
|
+
if (!prereqs || prereqs.length === 0) continue;
|
|
1950
|
+
const unmet = prereqs.filter(
|
|
1951
|
+
(pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo)
|
|
1952
|
+
);
|
|
1953
|
+
if (unmet.length > 0 && depth > 0) {
|
|
1954
|
+
walkedFurther = true;
|
|
1955
|
+
for (const prereq of unmet) {
|
|
1956
|
+
this.collectSupportTagsRecursive(
|
|
1957
|
+
prereq.tag,
|
|
1958
|
+
hierarchyConfigs,
|
|
1959
|
+
userTagElo,
|
|
1960
|
+
userGlobalElo,
|
|
1961
|
+
depth - 1,
|
|
1962
|
+
visited,
|
|
1963
|
+
out
|
|
1964
|
+
);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
if (!walkedFurther) {
|
|
1969
|
+
out.add(tag);
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
isHardGatedTag(tag) {
|
|
1973
|
+
return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
|
|
1974
|
+
}
|
|
1975
|
+
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
1976
|
+
if (!userTagElo) return false;
|
|
1977
|
+
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
|
|
1978
|
+
if (userTagElo.count < minCount) return false;
|
|
1979
|
+
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1980
|
+
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
1981
|
+
}
|
|
1982
|
+
if (prereq.masteryThreshold?.minCount !== void 0) {
|
|
1983
|
+
return true;
|
|
1984
|
+
}
|
|
1985
|
+
return userTagElo.score >= userGlobalElo;
|
|
1986
|
+
}
|
|
1418
1987
|
};
|
|
1419
1988
|
}
|
|
1420
1989
|
});
|
|
@@ -1519,7 +2088,7 @@ var init_srs = __esm({
|
|
|
1519
2088
|
]
|
|
1520
2089
|
};
|
|
1521
2090
|
});
|
|
1522
|
-
return scored.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
2091
|
+
return { cards: scored.sort((a, b) => b.score - a.score).slice(0, limit) };
|
|
1523
2092
|
}
|
|
1524
2093
|
/**
|
|
1525
2094
|
* Compute backlog pressure based on number of due reviews.
|
|
@@ -1777,14 +2346,14 @@ var hierarchyDefinition_exports = {};
|
|
|
1777
2346
|
__export(hierarchyDefinition_exports, {
|
|
1778
2347
|
default: () => HierarchyDefinitionNavigator
|
|
1779
2348
|
});
|
|
1780
|
-
var import_common6,
|
|
2349
|
+
var import_common6, DEFAULT_MIN_COUNT2, HierarchyDefinitionNavigator;
|
|
1781
2350
|
var init_hierarchyDefinition = __esm({
|
|
1782
2351
|
"src/core/navigators/filters/hierarchyDefinition.ts"() {
|
|
1783
2352
|
"use strict";
|
|
1784
2353
|
init_navigators();
|
|
1785
2354
|
import_common6 = require("@vue-skuilder/common");
|
|
1786
2355
|
init_logger();
|
|
1787
|
-
|
|
2356
|
+
DEFAULT_MIN_COUNT2 = 3;
|
|
1788
2357
|
HierarchyDefinitionNavigator = class extends ContentNavigator {
|
|
1789
2358
|
config;
|
|
1790
2359
|
/** Human-readable name for CardFilter interface */
|
|
@@ -1811,7 +2380,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1811
2380
|
*/
|
|
1812
2381
|
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
1813
2382
|
if (!userTagElo) return false;
|
|
1814
|
-
const minCount = prereq.masteryThreshold?.minCount ??
|
|
2383
|
+
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT2;
|
|
1815
2384
|
if (userTagElo.count < minCount) return false;
|
|
1816
2385
|
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1817
2386
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
@@ -1912,18 +2481,55 @@ var init_hierarchyDefinition = __esm({
|
|
|
1912
2481
|
}
|
|
1913
2482
|
return boosts;
|
|
1914
2483
|
}
|
|
2484
|
+
/**
|
|
2485
|
+
* Build a map of gated tag → max configured targetBoost for all *open* gates.
|
|
2486
|
+
*
|
|
2487
|
+
* When a gate opens (prereqs met), cards carrying the gated tag get boosted —
|
|
2488
|
+
* ensuring newly-unlocked content surfaces promptly. The boost is a static
|
|
2489
|
+
* multiplier; natural ELO/SRS deprioritization after interaction handles decay.
|
|
2490
|
+
*/
|
|
2491
|
+
getTargetBoosts(unlockedTags) {
|
|
2492
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
2493
|
+
const configKeys = Object.keys(this.config.prerequisites);
|
|
2494
|
+
const unlockedArr = [...unlockedTags];
|
|
2495
|
+
logger.info(
|
|
2496
|
+
`[HierarchyDefinition:targetBoost:trace] ${this.name} | configKeys=${configKeys.length}, unlocked=${unlockedArr.length} (${unlockedArr.slice(0, 5).join(", ")}${unlockedArr.length > 5 ? "..." : ""})`
|
|
2497
|
+
);
|
|
2498
|
+
for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
|
|
2499
|
+
if (!unlockedTags.has(tagId)) continue;
|
|
2500
|
+
logger.info(
|
|
2501
|
+
`[HierarchyDefinition:targetBoost:trace] UNLOCKED ${tagId}: ${prereqs.length} prereqs, raw=${JSON.stringify(prereqs.map((p) => ({ tag: p.tag, tb: p.targetBoost })))}`
|
|
2502
|
+
);
|
|
2503
|
+
for (const prereq of prereqs) {
|
|
2504
|
+
if (!prereq.targetBoost || prereq.targetBoost <= 1) continue;
|
|
2505
|
+
const existing = boosts.get(tagId) ?? 1;
|
|
2506
|
+
boosts.set(tagId, Math.max(existing, prereq.targetBoost));
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
if (boosts.size > 0) {
|
|
2510
|
+
logger.info(
|
|
2511
|
+
`[HierarchyDefinition] targetBoosts active: ${[...boosts.entries()].map(([t, b]) => `${t}=\xD7${b}`).join(", ")}`
|
|
2512
|
+
);
|
|
2513
|
+
} else {
|
|
2514
|
+
logger.info(
|
|
2515
|
+
`[HierarchyDefinition:targetBoost:trace] no targetBoosts found despite ${unlockedArr.length} unlocked tags`
|
|
2516
|
+
);
|
|
2517
|
+
}
|
|
2518
|
+
return boosts;
|
|
2519
|
+
}
|
|
1915
2520
|
/**
|
|
1916
2521
|
* CardFilter.transform implementation.
|
|
1917
2522
|
*
|
|
1918
|
-
*
|
|
1919
|
-
* 1. Cards with locked tags receive score * 0.
|
|
1920
|
-
* 2. Cards carrying prereq tags of closed gates receive
|
|
1921
|
-
*
|
|
2523
|
+
* Three effects:
|
|
2524
|
+
* 1. Cards with locked tags receive score * 0.02 (gating penalty)
|
|
2525
|
+
* 2. Cards carrying prereq tags of closed gates receive preReqBoost
|
|
2526
|
+
* 3. Cards carrying gated tags of open gates receive targetBoost
|
|
1922
2527
|
*/
|
|
1923
2528
|
async transform(cards, context) {
|
|
1924
2529
|
const masteredTags = await this.getMasteredTags(context);
|
|
1925
2530
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
1926
2531
|
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
2532
|
+
const targetBoosts = this.getTargetBoosts(unlockedTags);
|
|
1927
2533
|
const gated = [];
|
|
1928
2534
|
for (const card of cards) {
|
|
1929
2535
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
@@ -1956,6 +2562,26 @@ var init_hierarchyDefinition = __esm({
|
|
|
1956
2562
|
);
|
|
1957
2563
|
}
|
|
1958
2564
|
}
|
|
2565
|
+
if (isUnlocked && targetBoosts.size > 0) {
|
|
2566
|
+
const cardTags = card.tags ?? [];
|
|
2567
|
+
let maxTargetBoost = 1;
|
|
2568
|
+
const boostedTargets = [];
|
|
2569
|
+
for (const tag of cardTags) {
|
|
2570
|
+
const boost = targetBoosts.get(tag);
|
|
2571
|
+
if (boost && boost > maxTargetBoost) {
|
|
2572
|
+
maxTargetBoost = boost;
|
|
2573
|
+
boostedTargets.push(tag);
|
|
2574
|
+
}
|
|
2575
|
+
}
|
|
2576
|
+
if (maxTargetBoost > 1) {
|
|
2577
|
+
finalScore *= maxTargetBoost;
|
|
2578
|
+
action = "boosted";
|
|
2579
|
+
finalReason = `${finalReason} | targetBoost \xD7${maxTargetBoost.toFixed(2)} for ${boostedTargets.join(", ")}`;
|
|
2580
|
+
logger.info(
|
|
2581
|
+
`[HierarchyDefinition] targetBoost \xD7${maxTargetBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedTargets.join(", ")}] (score: ${card.score.toFixed(3)} \u2192 ${finalScore.toFixed(3)})`
|
|
2582
|
+
);
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
1959
2585
|
gated.push({
|
|
1960
2586
|
...card,
|
|
1961
2587
|
score: finalScore,
|
|
@@ -2140,13 +2766,13 @@ var interferenceMitigator_exports = {};
|
|
|
2140
2766
|
__export(interferenceMitigator_exports, {
|
|
2141
2767
|
default: () => InterferenceMitigatorNavigator
|
|
2142
2768
|
});
|
|
2143
|
-
var import_common7,
|
|
2769
|
+
var import_common7, DEFAULT_MIN_COUNT3, DEFAULT_MIN_ELAPSED_DAYS, DEFAULT_INTERFERENCE_DECAY, InterferenceMitigatorNavigator;
|
|
2144
2770
|
var init_interferenceMitigator = __esm({
|
|
2145
2771
|
"src/core/navigators/filters/interferenceMitigator.ts"() {
|
|
2146
2772
|
"use strict";
|
|
2147
2773
|
init_navigators();
|
|
2148
2774
|
import_common7 = require("@vue-skuilder/common");
|
|
2149
|
-
|
|
2775
|
+
DEFAULT_MIN_COUNT3 = 10;
|
|
2150
2776
|
DEFAULT_MIN_ELAPSED_DAYS = 3;
|
|
2151
2777
|
DEFAULT_INTERFERENCE_DECAY = 0.8;
|
|
2152
2778
|
InterferenceMitigatorNavigator = class extends ContentNavigator {
|
|
@@ -2171,7 +2797,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2171
2797
|
return {
|
|
2172
2798
|
interferenceSets: sets,
|
|
2173
2799
|
maturityThreshold: {
|
|
2174
|
-
minCount: parsed.maturityThreshold?.minCount ??
|
|
2800
|
+
minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3,
|
|
2175
2801
|
minElo: parsed.maturityThreshold?.minElo,
|
|
2176
2802
|
minElapsedDays: parsed.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS
|
|
2177
2803
|
},
|
|
@@ -2181,7 +2807,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2181
2807
|
return {
|
|
2182
2808
|
interferenceSets: [],
|
|
2183
2809
|
maturityThreshold: {
|
|
2184
|
-
minCount:
|
|
2810
|
+
minCount: DEFAULT_MIN_COUNT3,
|
|
2185
2811
|
minElapsedDays: DEFAULT_MIN_ELAPSED_DAYS
|
|
2186
2812
|
},
|
|
2187
2813
|
defaultDecay: DEFAULT_INTERFERENCE_DECAY
|
|
@@ -2228,7 +2854,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2228
2854
|
try {
|
|
2229
2855
|
const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
|
|
2230
2856
|
const userElo = (0, import_common7.toCourseElo)(courseReg.elo);
|
|
2231
|
-
const minCount = this.config.maturityThreshold?.minCount ??
|
|
2857
|
+
const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3;
|
|
2232
2858
|
const minElo = this.config.maturityThreshold?.minElo;
|
|
2233
2859
|
const minElapsedDays = this.config.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS;
|
|
2234
2860
|
const minCountForElapsed = minElapsedDays * 2;
|
|
@@ -2913,6 +3539,44 @@ function globMatch(value, pattern) {
|
|
|
2913
3539
|
function cardMatchesTagPattern(card, pattern) {
|
|
2914
3540
|
return (card.tags ?? []).some((tag) => globMatch(tag, pattern));
|
|
2915
3541
|
}
|
|
3542
|
+
function mergeHints2(allHints) {
|
|
3543
|
+
const defined = allHints.filter((h) => h !== null && h !== void 0);
|
|
3544
|
+
if (defined.length === 0) return void 0;
|
|
3545
|
+
const merged = {};
|
|
3546
|
+
const boostTags = {};
|
|
3547
|
+
for (const hints of defined) {
|
|
3548
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
|
|
3549
|
+
boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
if (Object.keys(boostTags).length > 0) {
|
|
3553
|
+
merged.boostTags = boostTags;
|
|
3554
|
+
}
|
|
3555
|
+
const boostCards = {};
|
|
3556
|
+
for (const hints of defined) {
|
|
3557
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
|
|
3558
|
+
boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
if (Object.keys(boostCards).length > 0) {
|
|
3562
|
+
merged.boostCards = boostCards;
|
|
3563
|
+
}
|
|
3564
|
+
const concatUnique = (field) => {
|
|
3565
|
+
const values = defined.flatMap((h) => h[field] ?? []);
|
|
3566
|
+
if (values.length > 0) {
|
|
3567
|
+
merged[field] = [...new Set(values)];
|
|
3568
|
+
}
|
|
3569
|
+
};
|
|
3570
|
+
concatUnique("requireTags");
|
|
3571
|
+
concatUnique("requireCards");
|
|
3572
|
+
concatUnique("excludeTags");
|
|
3573
|
+
concatUnique("excludeCards");
|
|
3574
|
+
const labels = defined.map((h) => h._label).filter(Boolean);
|
|
3575
|
+
if (labels.length > 0) {
|
|
3576
|
+
merged._label = labels.join("; ");
|
|
3577
|
+
}
|
|
3578
|
+
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
3579
|
+
}
|
|
2916
3580
|
function logPipelineConfig(generator, filters) {
|
|
2917
3581
|
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
2918
3582
|
logger.info(
|
|
@@ -3064,9 +3728,12 @@ var init_Pipeline = __esm({
|
|
|
3064
3728
|
logger.debug(
|
|
3065
3729
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
3066
3730
|
);
|
|
3067
|
-
|
|
3731
|
+
const generatorResult = await this.generator.getWeightedCards(fetchLimit, context);
|
|
3732
|
+
let cards = generatorResult.cards;
|
|
3068
3733
|
const tGenerate = performance.now();
|
|
3069
3734
|
const generatedCount = cards.length;
|
|
3735
|
+
const mergedHints = mergeHints2([this._ephemeralHints, generatorResult.hints]);
|
|
3736
|
+
this._ephemeralHints = mergedHints ?? null;
|
|
3070
3737
|
let generatorSummaries;
|
|
3071
3738
|
if (this.generator.generators) {
|
|
3072
3739
|
const genMap = /* @__PURE__ */ new Map();
|
|
@@ -3144,14 +3811,15 @@ var init_Pipeline = __esm({
|
|
|
3144
3811
|
generatorSummaries,
|
|
3145
3812
|
generatedCount,
|
|
3146
3813
|
filterImpacts,
|
|
3147
|
-
|
|
3148
|
-
result
|
|
3814
|
+
cards,
|
|
3815
|
+
result,
|
|
3816
|
+
context.userElo
|
|
3149
3817
|
);
|
|
3150
3818
|
captureRun(report);
|
|
3151
3819
|
} catch (e) {
|
|
3152
3820
|
logger.debug(`[Pipeline] Failed to capture debug run: ${e}`);
|
|
3153
3821
|
}
|
|
3154
|
-
return result;
|
|
3822
|
+
return { cards: result };
|
|
3155
3823
|
}
|
|
3156
3824
|
/**
|
|
3157
3825
|
* Batch hydrate tags for all cards.
|
|
@@ -3359,6 +4027,34 @@ var init_Pipeline = __esm({
|
|
|
3359
4027
|
return [...new Set(ids)];
|
|
3360
4028
|
}
|
|
3361
4029
|
// ---------------------------------------------------------------------------
|
|
4030
|
+
// Tag ELO diagnostic
|
|
4031
|
+
// ---------------------------------------------------------------------------
|
|
4032
|
+
/**
|
|
4033
|
+
* Get the user's per-tag ELO data for specified tags (or all tags).
|
|
4034
|
+
* Useful for diagnosing why hierarchy gates are open/closed.
|
|
4035
|
+
*/
|
|
4036
|
+
async getTagEloStatus(tagFilter) {
|
|
4037
|
+
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
4038
|
+
const courseElo = (0, import_common8.toCourseElo)(courseReg.elo);
|
|
4039
|
+
const result = {};
|
|
4040
|
+
if (!tagFilter) {
|
|
4041
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
4042
|
+
result[tag] = { score: data.score, count: data.count };
|
|
4043
|
+
}
|
|
4044
|
+
} else {
|
|
4045
|
+
const patterns = Array.isArray(tagFilter) ? tagFilter : [tagFilter];
|
|
4046
|
+
for (const pattern of patterns) {
|
|
4047
|
+
const regex = globToRegex(pattern);
|
|
4048
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
4049
|
+
if (regex.test(tag)) {
|
|
4050
|
+
result[tag] = { score: data.score, count: data.count };
|
|
4051
|
+
}
|
|
4052
|
+
}
|
|
4053
|
+
}
|
|
4054
|
+
}
|
|
4055
|
+
return result;
|
|
4056
|
+
}
|
|
4057
|
+
// ---------------------------------------------------------------------------
|
|
3362
4058
|
// Card-space diagnostic
|
|
3363
4059
|
// ---------------------------------------------------------------------------
|
|
3364
4060
|
/**
|
|
@@ -4750,7 +5446,7 @@ var init_classroomDB2 = __esm({
|
|
|
4750
5446
|
for (const content of due) {
|
|
4751
5447
|
if (content.type === "course") {
|
|
4752
5448
|
const db = new CourseDB(content.courseID, async () => this._user);
|
|
4753
|
-
const courseCards = await db.getWeightedCards(limit);
|
|
5449
|
+
const { cards: courseCards } = await db.getWeightedCards(limit);
|
|
4754
5450
|
for (const card of courseCards) {
|
|
4755
5451
|
if (!activeCardIds.has(card.cardId)) {
|
|
4756
5452
|
weighted.push({
|
|
@@ -4813,7 +5509,7 @@ var init_classroomDB2 = __esm({
|
|
|
4813
5509
|
logger.info(
|
|
4814
5510
|
`[StudentClassroomDB] New cards from classroom ${this._cfg.name}: ${weighted.length} total (reviews + new)`
|
|
4815
5511
|
);
|
|
4816
|
-
return weighted.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
5512
|
+
return { cards: weighted.sort((a, b) => b.score - a.score).slice(0, limit) };
|
|
4817
5513
|
}
|
|
4818
5514
|
};
|
|
4819
5515
|
}
|
|
@@ -6049,7 +6745,7 @@ var init_TagFilteredContentSource = __esm({
|
|
|
6049
6745
|
async getWeightedCards(limit) {
|
|
6050
6746
|
if (!(0, import_common14.hasActiveFilter)(this.filter)) {
|
|
6051
6747
|
logger.warn("[TagFilteredContentSource] getWeightedCards called with no active filter");
|
|
6052
|
-
return [];
|
|
6748
|
+
return { cards: [] };
|
|
6053
6749
|
}
|
|
6054
6750
|
const eligibleCardIds = await this.resolveFilteredCardIds();
|
|
6055
6751
|
const activeCards = await this.user.getActiveCards();
|
|
@@ -6101,7 +6797,7 @@ var init_TagFilteredContentSource = __esm({
|
|
|
6101
6797
|
}
|
|
6102
6798
|
]
|
|
6103
6799
|
}));
|
|
6104
|
-
return [...reviewWeighted, ...newCardWeighted].slice(0, limit);
|
|
6800
|
+
return { cards: [...reviewWeighted, ...newCardWeighted].slice(0, limit) };
|
|
6105
6801
|
}
|
|
6106
6802
|
/**
|
|
6107
6803
|
* Clears the cached resolved card IDs.
|