@vue-skuilder/db 0.1.32-e → 0.1.32-f
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/core/index.d.cts +20 -3
- package/dist/core/index.d.ts +20 -3
- package/dist/core/index.js +461 -30
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +461 -30
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +461 -30
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +461 -30
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +457 -28
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +457 -28
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +467 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +467 -32
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/core/navigators/Pipeline.ts +104 -13
- package/src/core/navigators/PipelineDebugger.ts +296 -3
- package/src/core/navigators/generators/CompositeGenerator.ts +4 -1
- package/src/core/navigators/generators/prescribed.ts +246 -22
- package/src/impl/couch/courseDB.ts +3 -2
- package/src/study/SessionController.ts +1 -0
- package/src/study/services/CardHydrationService.ts +6 -1
- package/tests/core/navigators/CompositeGenerator.test.ts +14 -50
- package/tests/core/navigators/Pipeline.test.ts +13 -12
package/dist/index.js
CHANGED
|
@@ -888,8 +888,9 @@ function getOrigin(card) {
|
|
|
888
888
|
const firstEntry = card.provenance[0];
|
|
889
889
|
if (!firstEntry) return "unknown";
|
|
890
890
|
const reason = firstEntry.reason?.toLowerCase() || "";
|
|
891
|
-
|
|
892
|
-
if (reason.includes("
|
|
891
|
+
const strategy = firstEntry.strategy?.toLowerCase() || "";
|
|
892
|
+
if (reason.includes("new card") || strategy.includes("elo")) return "new";
|
|
893
|
+
if (reason.includes("review") || strategy.includes("srs")) return "review";
|
|
893
894
|
return "unknown";
|
|
894
895
|
}
|
|
895
896
|
function captureRun(report) {
|
|
@@ -909,12 +910,13 @@ function parseCardElo(provenance) {
|
|
|
909
910
|
const match = eloEntry.reason.match(/card:\s*(\d+)/);
|
|
910
911
|
return match ? parseInt(match[1], 10) : void 0;
|
|
911
912
|
}
|
|
912
|
-
function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
|
|
913
|
+
function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
|
|
913
914
|
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
914
915
|
const cards = allCards.map((card) => ({
|
|
915
916
|
cardId: card.cardId,
|
|
916
917
|
courseId: card.courseId,
|
|
917
918
|
origin: getOrigin(card),
|
|
919
|
+
generator: card.provenance[0]?.strategyName || card.provenance[0]?.strategy,
|
|
918
920
|
finalScore: card.score,
|
|
919
921
|
cardElo: parseCardElo(card.provenance),
|
|
920
922
|
provenance: card.provenance,
|
|
@@ -931,6 +933,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
931
933
|
generators,
|
|
932
934
|
generatedCount,
|
|
933
935
|
filters,
|
|
936
|
+
hints,
|
|
934
937
|
finalCount: selectedCards.length,
|
|
935
938
|
reviewsSelected,
|
|
936
939
|
newSelected,
|
|
@@ -970,13 +973,169 @@ function printRunSummary(run) {
|
|
|
970
973
|
);
|
|
971
974
|
console.groupEnd();
|
|
972
975
|
}
|
|
976
|
+
function renderUI() {
|
|
977
|
+
if (!_uiContainer) return;
|
|
978
|
+
const runs = runHistory;
|
|
979
|
+
const selectedRun = _selectedRunIndex !== null ? runs[_selectedRunIndex] : null;
|
|
980
|
+
const styles = `
|
|
981
|
+
#sk-pipeline-debugger {
|
|
982
|
+
position: fixed;
|
|
983
|
+
top: 0;
|
|
984
|
+
left: 0;
|
|
985
|
+
width: 100vw;
|
|
986
|
+
height: 100vh;
|
|
987
|
+
background: #f8f9fa;
|
|
988
|
+
color: #212529;
|
|
989
|
+
z-index: 999999;
|
|
990
|
+
display: flex;
|
|
991
|
+
flex-direction: column;
|
|
992
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
993
|
+
font-size: 14px;
|
|
994
|
+
}
|
|
995
|
+
#sk-pipeline-debugger header {
|
|
996
|
+
padding: 1rem;
|
|
997
|
+
background: #343a40;
|
|
998
|
+
color: white;
|
|
999
|
+
display: flex;
|
|
1000
|
+
justify-content: space-between;
|
|
1001
|
+
align-items: center;
|
|
1002
|
+
}
|
|
1003
|
+
#sk-pipeline-debugger .container {
|
|
1004
|
+
display: flex;
|
|
1005
|
+
flex: 1;
|
|
1006
|
+
overflow: hidden;
|
|
1007
|
+
}
|
|
1008
|
+
#sk-pipeline-debugger .sidebar {
|
|
1009
|
+
width: 300px;
|
|
1010
|
+
border-right: 1px solid #dee2e6;
|
|
1011
|
+
overflow-y: auto;
|
|
1012
|
+
background: white;
|
|
1013
|
+
}
|
|
1014
|
+
#sk-pipeline-debugger .main-content {
|
|
1015
|
+
flex: 1;
|
|
1016
|
+
overflow-y: auto;
|
|
1017
|
+
padding: 1.5rem;
|
|
1018
|
+
}
|
|
1019
|
+
#sk-pipeline-debugger .run-item {
|
|
1020
|
+
padding: 0.75rem 1rem;
|
|
1021
|
+
border-bottom: 1px solid #eee;
|
|
1022
|
+
cursor: pointer;
|
|
1023
|
+
}
|
|
1024
|
+
#sk-pipeline-debugger .run-item:hover { background: #f1f3f5; }
|
|
1025
|
+
#sk-pipeline-debugger .run-item.active { background: #e9ecef; border-left: 4px solid #007bff; }
|
|
1026
|
+
#sk-pipeline-debugger h2, #sk-pipeline-debugger h3 { margin-top: 0; }
|
|
1027
|
+
#sk-pipeline-debugger table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; background: white; }
|
|
1028
|
+
#sk-pipeline-debugger th, #sk-pipeline-debugger td { border: 1px solid #dee2e6; padding: 0.5rem; text-align: left; }
|
|
1029
|
+
#sk-pipeline-debugger th { background: #f1f3f5; }
|
|
1030
|
+
#sk-pipeline-debugger code { background: #f1f3f5; padding: 0.1rem 0.3rem; border-radius: 3px; font-family: monospace; }
|
|
1031
|
+
#sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
|
|
1032
|
+
#sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
|
|
1033
|
+
#sk-pipeline-debugger .provenance { font-size: 12px; color: #666; margin-top: 0.25rem; white-space: pre-wrap; font-family: monospace; background: #f8f9fa; padding: 0.5rem; border-radius: 4px; }
|
|
1034
|
+
`;
|
|
1035
|
+
const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
|
|
1036
|
+
(r, i) => `
|
|
1037
|
+
<div class="run-item ${i === _selectedRunIndex ? "active" : ""}" onclick="window.skuilder.pipeline._selectRun(${i})">
|
|
1038
|
+
<strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
|
|
1039
|
+
<small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
|
|
1040
|
+
<small>${r.finalCount} cards selected</small>
|
|
1041
|
+
</div>
|
|
1042
|
+
`
|
|
1043
|
+
).join("");
|
|
1044
|
+
let detailsHtml = '<div style="color: #6c757d; text-align: center; margin-top: 5rem;">Select a run to see details</div>';
|
|
1045
|
+
if (selectedRun) {
|
|
1046
|
+
const filteredCards = selectedRun.cards.filter(
|
|
1047
|
+
(c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
|
|
1048
|
+
);
|
|
1049
|
+
detailsHtml = `
|
|
1050
|
+
<h2>Run: ${selectedRun.runId}</h2>
|
|
1051
|
+
<p>
|
|
1052
|
+
<strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
|
|
1053
|
+
<strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
|
|
1054
|
+
<strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
|
|
1055
|
+
</p>
|
|
1056
|
+
|
|
1057
|
+
<h3>Pipeline Config</h3>
|
|
1058
|
+
<table>
|
|
1059
|
+
<tr><th>Generator</th><td>${selectedRun.generatorName} (${selectedRun.generatedCount} candidates)</td></tr>
|
|
1060
|
+
${(selectedRun.generators || []).map(
|
|
1061
|
+
(g) => `
|
|
1062
|
+
<tr><td style="padding-left: 2rem;">\u21B3 ${g.name}</td><td>${g.cardCount} cards (${g.newCount} new, ${g.reviewCount} review, top: ${g.topScore.toFixed(2)})</td></tr>
|
|
1063
|
+
`
|
|
1064
|
+
).join("")}
|
|
1065
|
+
</table>
|
|
1066
|
+
|
|
1067
|
+
${selectedRun.hints ? `
|
|
1068
|
+
<h3>Ephemeral Hints</h3>
|
|
1069
|
+
<table>
|
|
1070
|
+
${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
|
|
1071
|
+
${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
|
|
1072
|
+
${selectedRun.hints.boostCards ? `<tr><th>Boost Cards</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostCards, null, 2)}</pre></td></tr>` : ""}
|
|
1073
|
+
${selectedRun.hints.requireTags ? `<tr><th>Require Tags</th><td>${selectedRun.hints.requireTags.join(", ")}</td></tr>` : ""}
|
|
1074
|
+
${selectedRun.hints.requireCards ? `<tr><th>Require Cards</th><td>${selectedRun.hints.requireCards.join(", ")}</td></tr>` : ""}
|
|
1075
|
+
${selectedRun.hints.excludeTags ? `<tr><th>Exclude Tags</th><td>${selectedRun.hints.excludeTags.join(", ")}</td></tr>` : ""}
|
|
1076
|
+
${selectedRun.hints.excludeCards ? `<tr><th>Exclude Cards</th><td>${selectedRun.hints.excludeCards.join(", ")}</td></tr>` : ""}
|
|
1077
|
+
</table>
|
|
1078
|
+
` : ""}
|
|
1079
|
+
|
|
1080
|
+
<h3>Filter Impact</h3>
|
|
1081
|
+
<table>
|
|
1082
|
+
<thead><tr><th>Filter</th><th>Boosted</th><th>Penalized</th><th>Passed</th><th>Removed</th></tr></thead>
|
|
1083
|
+
<tbody>
|
|
1084
|
+
${selectedRun.filters.map(
|
|
1085
|
+
(f) => `
|
|
1086
|
+
<tr><td>${f.name}</td><td>\u2191${f.boosted}</td><td>\u2193${f.penalized}</td><td>=${f.passed}</td><td>\u2715${f.removed}</td></tr>
|
|
1087
|
+
`
|
|
1088
|
+
).join("")}
|
|
1089
|
+
</tbody>
|
|
1090
|
+
</table>
|
|
1091
|
+
|
|
1092
|
+
<h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
|
|
1093
|
+
<input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
|
|
1094
|
+
|
|
1095
|
+
<table>
|
|
1096
|
+
<thead><tr><th>ID</th><th>Generator</th><th>Origin</th><th>Score</th><th>Selected</th></tr></thead>
|
|
1097
|
+
<tbody>
|
|
1098
|
+
${filteredCards.map(
|
|
1099
|
+
(c) => `
|
|
1100
|
+
<tr>
|
|
1101
|
+
<td><code>${c.cardId}</code></td>
|
|
1102
|
+
<td>${c.generator || "unknown"}</td>
|
|
1103
|
+
<td>${c.origin}</td>
|
|
1104
|
+
<td>${c.finalScore.toFixed(3)}</td>
|
|
1105
|
+
<td>${c.selected ? "\u2705" : "\u274C"}</td>
|
|
1106
|
+
</tr>
|
|
1107
|
+
${c.selected || _cardSearchQuery ? `
|
|
1108
|
+
<tr>
|
|
1109
|
+
<td colspan="5">
|
|
1110
|
+
<div class="provenance">${formatProvenance(c.provenance)}</div>
|
|
1111
|
+
</td>
|
|
1112
|
+
</tr>
|
|
1113
|
+
` : ""}
|
|
1114
|
+
`
|
|
1115
|
+
).join("")}
|
|
1116
|
+
</tbody>
|
|
1117
|
+
</table>
|
|
1118
|
+
`;
|
|
1119
|
+
}
|
|
1120
|
+
_uiContainer.innerHTML = `
|
|
1121
|
+
<style>${styles}</style>
|
|
1122
|
+
<header>
|
|
1123
|
+
<strong>Pipeline Debugger</strong>
|
|
1124
|
+
<button class="close-btn" onclick="window.skuilder.pipeline.ui()">Close</button>
|
|
1125
|
+
</header>
|
|
1126
|
+
<div class="container">
|
|
1127
|
+
<div class="sidebar">${runListHtml}</div>
|
|
1128
|
+
<div class="main-content">${detailsHtml}</div>
|
|
1129
|
+
</div>
|
|
1130
|
+
`;
|
|
1131
|
+
}
|
|
973
1132
|
function mountPipelineDebugger() {
|
|
974
1133
|
if (typeof window === "undefined") return;
|
|
975
1134
|
const win = window;
|
|
976
1135
|
win.skuilder = win.skuilder || {};
|
|
977
1136
|
win.skuilder.pipeline = pipelineDebugAPI;
|
|
978
1137
|
}
|
|
979
|
-
var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
|
|
1138
|
+
var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
|
|
980
1139
|
var init_PipelineDebugger = __esm({
|
|
981
1140
|
"src/core/navigators/PipelineDebugger.ts"() {
|
|
982
1141
|
"use strict";
|
|
@@ -985,6 +1144,9 @@ var init_PipelineDebugger = __esm({
|
|
|
985
1144
|
_activePipeline = null;
|
|
986
1145
|
MAX_RUNS = 10;
|
|
987
1146
|
runHistory = [];
|
|
1147
|
+
_uiContainer = null;
|
|
1148
|
+
_selectedRunIndex = null;
|
|
1149
|
+
_cardSearchQuery = "";
|
|
988
1150
|
pipelineDebugAPI = {
|
|
989
1151
|
/**
|
|
990
1152
|
* Get raw run history for programmatic access.
|
|
@@ -1124,16 +1286,20 @@ var init_PipelineDebugger = __esm({
|
|
|
1124
1286
|
const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
|
|
1125
1287
|
const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
|
|
1126
1288
|
const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
|
|
1289
|
+
const supportCard = reason.match(/supportCard=([^;]+)/)?.[1] ?? "none";
|
|
1127
1290
|
const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
|
|
1128
1291
|
const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
|
|
1292
|
+
const supportSource = mode === "discovered-support" ? "discovered" : mode === "support" ? "authored" : "n/a";
|
|
1129
1293
|
return {
|
|
1130
1294
|
group: parsedGroup,
|
|
1131
1295
|
mode,
|
|
1296
|
+
supportSource,
|
|
1132
1297
|
cardId: card.cardId,
|
|
1133
1298
|
selected: card.selected ? "yes" : "no",
|
|
1134
1299
|
finalScore: card.finalScore.toFixed(3),
|
|
1135
1300
|
blocked,
|
|
1136
1301
|
blockedTargets,
|
|
1302
|
+
supportCard,
|
|
1137
1303
|
supportTags,
|
|
1138
1304
|
multiplier
|
|
1139
1305
|
};
|
|
@@ -1149,6 +1315,8 @@ var init_PipelineDebugger = __esm({
|
|
|
1149
1315
|
const selectedRows = rows.filter((r) => r.selected === "yes");
|
|
1150
1316
|
const blockedTargetSet = /* @__PURE__ */ new Set();
|
|
1151
1317
|
const supportTagSet = /* @__PURE__ */ new Set();
|
|
1318
|
+
const authoredSupportSet = /* @__PURE__ */ new Set();
|
|
1319
|
+
const discoveredSupportSet = /* @__PURE__ */ new Set();
|
|
1152
1320
|
for (const row of rows) {
|
|
1153
1321
|
if (row.blockedTargets && row.blockedTargets !== "none") {
|
|
1154
1322
|
row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
|
|
@@ -1156,6 +1324,13 @@ var init_PipelineDebugger = __esm({
|
|
|
1156
1324
|
if (row.supportTags && row.supportTags !== "none") {
|
|
1157
1325
|
row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
|
|
1158
1326
|
}
|
|
1327
|
+
if (row.supportCard && row.supportCard !== "none") {
|
|
1328
|
+
if (row.supportSource === "discovered") {
|
|
1329
|
+
discoveredSupportSet.add(row.supportCard);
|
|
1330
|
+
} else if (row.supportSource === "authored") {
|
|
1331
|
+
authoredSupportSet.add(row.supportCard);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1159
1334
|
}
|
|
1160
1335
|
logger.info(`Prescribed cards in run: ${rows.length}`);
|
|
1161
1336
|
logger.info(`Selected prescribed cards: ${selectedRows.length}`);
|
|
@@ -1165,6 +1340,12 @@ var init_PipelineDebugger = __esm({
|
|
|
1165
1340
|
logger.info(
|
|
1166
1341
|
`Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
|
|
1167
1342
|
);
|
|
1343
|
+
logger.info(
|
|
1344
|
+
`Authored support cards emitted: ${authoredSupportSet.size > 0 ? [...authoredSupportSet].join(", ") : "none"}`
|
|
1345
|
+
);
|
|
1346
|
+
logger.info(
|
|
1347
|
+
`Discovered support cards emitted: ${discoveredSupportSet.size > 0 ? [...discoveredSupportSet].join(", ") : "none"}`
|
|
1348
|
+
);
|
|
1168
1349
|
console.groupEnd();
|
|
1169
1350
|
},
|
|
1170
1351
|
/**
|
|
@@ -1299,6 +1480,39 @@ var init_PipelineDebugger = __esm({
|
|
|
1299
1480
|
Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
|
|
1300
1481
|
);
|
|
1301
1482
|
},
|
|
1483
|
+
/**
|
|
1484
|
+
* Toggle the full-screen UI debugger.
|
|
1485
|
+
*/
|
|
1486
|
+
ui() {
|
|
1487
|
+
if (_uiContainer) {
|
|
1488
|
+
document.body.removeChild(_uiContainer);
|
|
1489
|
+
_uiContainer = null;
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1492
|
+
_uiContainer = document.createElement("div");
|
|
1493
|
+
_uiContainer.id = "sk-pipeline-debugger";
|
|
1494
|
+
document.body.appendChild(_uiContainer);
|
|
1495
|
+
if (_selectedRunIndex === null && runHistory.length > 0) {
|
|
1496
|
+
_selectedRunIndex = 0;
|
|
1497
|
+
}
|
|
1498
|
+
renderUI();
|
|
1499
|
+
},
|
|
1500
|
+
/**
|
|
1501
|
+
* Internal UI helpers
|
|
1502
|
+
* @internal
|
|
1503
|
+
*/
|
|
1504
|
+
_selectRun(index) {
|
|
1505
|
+
_selectedRunIndex = index;
|
|
1506
|
+
renderUI();
|
|
1507
|
+
},
|
|
1508
|
+
/**
|
|
1509
|
+
* Internal UI helpers
|
|
1510
|
+
* @internal
|
|
1511
|
+
*/
|
|
1512
|
+
_setSearch(query) {
|
|
1513
|
+
_cardSearchQuery = query;
|
|
1514
|
+
renderUI();
|
|
1515
|
+
},
|
|
1302
1516
|
/**
|
|
1303
1517
|
* Show help.
|
|
1304
1518
|
*/
|
|
@@ -1307,6 +1521,7 @@ var init_PipelineDebugger = __esm({
|
|
|
1307
1521
|
\u{1F527} Pipeline Debug API
|
|
1308
1522
|
|
|
1309
1523
|
Commands:
|
|
1524
|
+
.ui() Toggle full-screen UI debugger
|
|
1310
1525
|
.showLastRun() Show summary of most recent pipeline run
|
|
1311
1526
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
1312
1527
|
.showCard(cardId) Show provenance trail for a specific card
|
|
@@ -1323,6 +1538,7 @@ Commands:
|
|
|
1323
1538
|
.help() Show this help message
|
|
1324
1539
|
|
|
1325
1540
|
Example:
|
|
1541
|
+
window.skuilder.pipeline.ui()
|
|
1326
1542
|
window.skuilder.pipeline.showLastRun()
|
|
1327
1543
|
window.skuilder.pipeline.showRun(1)
|
|
1328
1544
|
await window.skuilder.pipeline.diagnoseCardSpace()
|
|
@@ -1485,7 +1701,7 @@ var init_CompositeGenerator = __esm({
|
|
|
1485
1701
|
for (const [, items] of byCardId) {
|
|
1486
1702
|
const cards2 = items.map((i) => i.card);
|
|
1487
1703
|
const aggregatedScore = this.aggregateScores(items);
|
|
1488
|
-
const finalScore = Math.
|
|
1704
|
+
const finalScore = Math.max(0, aggregatedScore);
|
|
1489
1705
|
const mergedProvenance = cards2.flatMap((c) => c.provenance);
|
|
1490
1706
|
const initialScore = cards2[0].score;
|
|
1491
1707
|
const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
|
|
@@ -1682,10 +1898,26 @@ function matchesTagPattern(tag, pattern) {
|
|
|
1682
1898
|
const re = new RegExp(`^${escaped}$`);
|
|
1683
1899
|
return re.test(tag);
|
|
1684
1900
|
}
|
|
1901
|
+
function extractWordStem(cardId) {
|
|
1902
|
+
for (const prefix of ["c-ml-", "c-ws-", "c-spelling-"]) {
|
|
1903
|
+
if (cardId.startsWith(prefix)) {
|
|
1904
|
+
const rest = cardId.slice(prefix.length);
|
|
1905
|
+
const lastDash = rest.lastIndexOf("-");
|
|
1906
|
+
return lastDash > 0 ? rest.slice(0, lastDash) : rest;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
return cardId;
|
|
1910
|
+
}
|
|
1911
|
+
function shuffleInPlace(arr) {
|
|
1912
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
1913
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1914
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1685
1917
|
function pickTopByScore(cards, limit) {
|
|
1686
1918
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1687
1919
|
}
|
|
1688
|
-
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,
|
|
1920
|
+
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, DISCOVERED_SUPPORT_SCORE, MAX_TARGET_MULTIPLIER, MAX_SUPPORT_MULTIPLIER, PRESCRIBED_DEBUG_VERSION, PrescribedCardsGenerator;
|
|
1689
1921
|
var init_prescribed = __esm({
|
|
1690
1922
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1691
1923
|
"use strict";
|
|
@@ -1698,11 +1930,10 @@ var init_prescribed = __esm({
|
|
|
1698
1930
|
DEFAULT_MIN_COUNT = 3;
|
|
1699
1931
|
BASE_TARGET_SCORE = 1;
|
|
1700
1932
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1933
|
+
DISCOVERED_SUPPORT_SCORE = 12;
|
|
1701
1934
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1702
1935
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1703
|
-
|
|
1704
|
-
LESSON_GATE_PENALTY_TAG_HINT = "concept:";
|
|
1705
|
-
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
|
|
1936
|
+
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
1706
1937
|
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1707
1938
|
name;
|
|
1708
1939
|
config;
|
|
@@ -1738,6 +1969,20 @@ var init_prescribed = __esm({
|
|
|
1738
1969
|
const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
|
|
1739
1970
|
const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
|
|
1740
1971
|
const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
|
|
1972
|
+
const courseTagDocs = await this.course.getCourseTagStubs().catch(
|
|
1973
|
+
() => ({
|
|
1974
|
+
rows: [],
|
|
1975
|
+
offset: 0,
|
|
1976
|
+
total_rows: 0
|
|
1977
|
+
})
|
|
1978
|
+
);
|
|
1979
|
+
const cardsByTag = /* @__PURE__ */ new Map();
|
|
1980
|
+
for (const row of courseTagDocs.rows ?? []) {
|
|
1981
|
+
const tagDoc = row.doc;
|
|
1982
|
+
if (tagDoc?.name && Array.isArray(tagDoc.taggedCards)) {
|
|
1983
|
+
cardsByTag.set(tagDoc.name, [...tagDoc.taggedCards]);
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1741
1986
|
const nextState = {
|
|
1742
1987
|
updatedAt: isoNow(),
|
|
1743
1988
|
groups: {}
|
|
@@ -1752,11 +1997,31 @@ var init_prescribed = __esm({
|
|
|
1752
1997
|
activeIds,
|
|
1753
1998
|
seenIds,
|
|
1754
1999
|
tagsByCard,
|
|
2000
|
+
cardsByTag,
|
|
1755
2001
|
hierarchyConfigs,
|
|
1756
2002
|
userTagElo,
|
|
1757
2003
|
userGlobalElo
|
|
1758
2004
|
});
|
|
1759
2005
|
groupRuntimes.push(runtime);
|
|
2006
|
+
logger.info(
|
|
2007
|
+
`[Prescribed] Group '${group.id}': ${group.targetCardIds.length} targets total, ${runtime.encounteredTargets.size} encountered, ${runtime.pendingTargets.length} pending (${runtime.surfaceableTargets.length} surfaceable, ${runtime.blockedTargets.length} blocked), ${runtime.supportCandidates.length} authored support candidates, ${runtime.discoveredSupportCandidates.length} discovered support candidates, pressure=${runtime.pressureMultiplier.toFixed(2)}`
|
|
2008
|
+
);
|
|
2009
|
+
if (runtime.blockedTargets.length > 0) {
|
|
2010
|
+
logger.info(
|
|
2011
|
+
`[Prescribed] Group '${group.id}' blocked targets: ${runtime.blockedTargets.join(", ")}`
|
|
2012
|
+
);
|
|
2013
|
+
logger.info(
|
|
2014
|
+
`[Prescribed] Group '${group.id}' support tags needed: ${runtime.supportTags.join(", ") || "(none)"}`
|
|
2015
|
+
);
|
|
2016
|
+
logger.info(
|
|
2017
|
+
`[Prescribed] Group '${group.id}' escalation mode: ` + (runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support-candidates" : "boost-only")
|
|
2018
|
+
);
|
|
2019
|
+
if (runtime.discoveredSupportCandidates.length > 0) {
|
|
2020
|
+
logger.info(
|
|
2021
|
+
`[Prescribed] Group '${group.id}' discovered support candidates: ${runtime.discoveredSupportCandidates.join(", ")}`
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
1760
2025
|
nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
|
|
1761
2026
|
const directCards = this.buildDirectTargetCards(
|
|
1762
2027
|
runtime,
|
|
@@ -1768,15 +2033,30 @@ var init_prescribed = __esm({
|
|
|
1768
2033
|
courseId,
|
|
1769
2034
|
emittedIds
|
|
1770
2035
|
);
|
|
1771
|
-
|
|
2036
|
+
const discoveredSupportCards = this.buildDiscoveredSupportCards(
|
|
2037
|
+
runtime,
|
|
2038
|
+
courseId,
|
|
2039
|
+
emittedIds
|
|
2040
|
+
);
|
|
2041
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
|
|
1772
2042
|
}
|
|
1773
2043
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
1774
2044
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
1775
2045
|
boostTags: hintSummary.boostTags,
|
|
1776
2046
|
_label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
|
|
1777
2047
|
} : void 0;
|
|
2048
|
+
if (hints) {
|
|
2049
|
+
const tagEntries = Object.entries(hints.boostTags ?? {});
|
|
2050
|
+
logger.info(
|
|
2051
|
+
`[Prescribed] Emitting ${tagEntries.length} boost hint(s): ` + tagEntries.map(([tag, mult]) => `${tag}\xD7${mult.toFixed(1)}`).join(", ")
|
|
2052
|
+
);
|
|
2053
|
+
} else {
|
|
2054
|
+
logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
|
|
2055
|
+
}
|
|
1778
2056
|
if (emitted.length === 0) {
|
|
1779
|
-
logger.
|
|
2057
|
+
logger.info(
|
|
2058
|
+
"[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
|
|
2059
|
+
);
|
|
1780
2060
|
await this.putStrategyState(nextState).catch((e) => {
|
|
1781
2061
|
logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
|
|
1782
2062
|
});
|
|
@@ -1809,7 +2089,7 @@ var init_prescribed = __esm({
|
|
|
1809
2089
|
logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
|
|
1810
2090
|
});
|
|
1811
2091
|
logger.info(
|
|
1812
|
-
`[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)`
|
|
2092
|
+
`[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, ${finalCards.filter((c) => c.provenance[0]?.reason.includes("mode=discovered-support")).length} discovered support)`
|
|
1813
2093
|
);
|
|
1814
2094
|
return hints ? { cards: finalCards, hints } : { cards: finalCards };
|
|
1815
2095
|
}
|
|
@@ -1839,9 +2119,15 @@ var init_prescribed = __esm({
|
|
|
1839
2119
|
const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
|
|
1840
2120
|
const groups = groupsRaw.map((raw, i) => ({
|
|
1841
2121
|
id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
|
|
1842
|
-
targetCardIds: dedupe(
|
|
1843
|
-
|
|
1844
|
-
|
|
2122
|
+
targetCardIds: dedupe(
|
|
2123
|
+
Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []
|
|
2124
|
+
),
|
|
2125
|
+
supportCardIds: dedupe(
|
|
2126
|
+
Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []
|
|
2127
|
+
),
|
|
2128
|
+
supportTagPatterns: dedupe(
|
|
2129
|
+
Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []
|
|
2130
|
+
),
|
|
1845
2131
|
freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
|
|
1846
2132
|
maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
|
|
1847
2133
|
maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
|
|
@@ -1858,7 +2144,7 @@ var init_prescribed = __esm({
|
|
|
1858
2144
|
}
|
|
1859
2145
|
async loadHierarchyConfigs() {
|
|
1860
2146
|
try {
|
|
1861
|
-
const strategies = await this.course.
|
|
2147
|
+
const strategies = await this.course.getAllNavigationStrategies();
|
|
1862
2148
|
return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
|
|
1863
2149
|
try {
|
|
1864
2150
|
const parsed = JSON.parse(s.serializedData);
|
|
@@ -1881,6 +2167,7 @@ var init_prescribed = __esm({
|
|
|
1881
2167
|
activeIds,
|
|
1882
2168
|
seenIds,
|
|
1883
2169
|
tagsByCard,
|
|
2170
|
+
cardsByTag,
|
|
1884
2171
|
hierarchyConfigs,
|
|
1885
2172
|
userTagElo,
|
|
1886
2173
|
userGlobalElo
|
|
@@ -1944,6 +2231,22 @@ var init_prescribed = __esm({
|
|
|
1944
2231
|
[...supportTags]
|
|
1945
2232
|
)
|
|
1946
2233
|
]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
|
|
2234
|
+
const discoveredSupportCandidates = blockedTargets.length > 0 && supportTags.size > 0 && supportCandidates.length === 0 ? this.findDiscoveredSupportCards({
|
|
2235
|
+
supportTags: [...supportTags],
|
|
2236
|
+
cardsByTag,
|
|
2237
|
+
activeIds,
|
|
2238
|
+
seenIds,
|
|
2239
|
+
excludedIds: /* @__PURE__ */ new Set([
|
|
2240
|
+
...group.targetCardIds,
|
|
2241
|
+
...group.supportCardIds ?? []
|
|
2242
|
+
]),
|
|
2243
|
+
limit: group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN
|
|
2244
|
+
}) : [];
|
|
2245
|
+
if (blockedTargets.length > 0 && supportTags.size > 0 && discoveredSupportCandidates.length === 0) {
|
|
2246
|
+
logger.info(
|
|
2247
|
+
`[Prescribed] Group '${group.id}' discovered 0 broader support candidates (blocked=${blockedTargets.length}; authoredSupport=${supportCandidates.length})`
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
1947
2250
|
const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
|
|
1948
2251
|
const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
1949
2252
|
const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
|
|
@@ -1957,6 +2260,7 @@ var init_prescribed = __esm({
|
|
|
1957
2260
|
surfaceableTargets,
|
|
1958
2261
|
targetTags,
|
|
1959
2262
|
supportCandidates,
|
|
2263
|
+
discoveredSupportCandidates,
|
|
1960
2264
|
supportTags: [...supportTags],
|
|
1961
2265
|
pressureMultiplier,
|
|
1962
2266
|
supportMultiplier,
|
|
@@ -2027,6 +2331,33 @@ var init_prescribed = __esm({
|
|
|
2027
2331
|
}
|
|
2028
2332
|
return cards;
|
|
2029
2333
|
}
|
|
2334
|
+
buildDiscoveredSupportCards(runtime, courseId, emittedIds) {
|
|
2335
|
+
if (runtime.blockedTargets.length === 0 || runtime.discoveredSupportCandidates.length === 0) {
|
|
2336
|
+
return [];
|
|
2337
|
+
}
|
|
2338
|
+
const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
|
|
2339
|
+
const supportIds = runtime.discoveredSupportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
|
|
2340
|
+
const cards = [];
|
|
2341
|
+
for (const cardId of supportIds) {
|
|
2342
|
+
emittedIds.add(cardId);
|
|
2343
|
+
cards.push({
|
|
2344
|
+
cardId,
|
|
2345
|
+
courseId,
|
|
2346
|
+
score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
2347
|
+
provenance: [
|
|
2348
|
+
{
|
|
2349
|
+
strategy: "prescribed",
|
|
2350
|
+
strategyName: this.strategyName || this.name,
|
|
2351
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2352
|
+
action: "generated",
|
|
2353
|
+
score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
2354
|
+
reason: `mode=discovered-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}`
|
|
2355
|
+
}
|
|
2356
|
+
]
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
return cards;
|
|
2360
|
+
}
|
|
2030
2361
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2031
2362
|
if (supportTags.length === 0) {
|
|
2032
2363
|
return [];
|
|
@@ -2049,6 +2380,40 @@ var init_prescribed = __esm({
|
|
|
2049
2380
|
}
|
|
2050
2381
|
return [...candidates];
|
|
2051
2382
|
}
|
|
2383
|
+
findDiscoveredSupportCards(args) {
|
|
2384
|
+
const { supportTags, cardsByTag, activeIds, seenIds, excludedIds, limit } = args;
|
|
2385
|
+
const byCardId = /* @__PURE__ */ new Map();
|
|
2386
|
+
for (const supportTag of supportTags) {
|
|
2387
|
+
const taggedCards = cardsByTag.get(supportTag) ?? [];
|
|
2388
|
+
for (const cardId of taggedCards) {
|
|
2389
|
+
if (activeIds.has(cardId) || seenIds.has(cardId) || excludedIds.has(cardId)) {
|
|
2390
|
+
continue;
|
|
2391
|
+
}
|
|
2392
|
+
const existing = byCardId.get(cardId);
|
|
2393
|
+
if (existing) {
|
|
2394
|
+
existing.matches += 1;
|
|
2395
|
+
} else {
|
|
2396
|
+
byCardId.set(cardId, { cardId, matches: 1 });
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
const candidates = [...byCardId.values()].sort((a, b) => b.matches - a.matches || a.cardId.localeCompare(b.cardId));
|
|
2401
|
+
const usedStems = /* @__PURE__ */ new Set();
|
|
2402
|
+
const diverse = [];
|
|
2403
|
+
const deferred = [];
|
|
2404
|
+
for (const entry of candidates) {
|
|
2405
|
+
const stem = extractWordStem(entry.cardId);
|
|
2406
|
+
if (!usedStems.has(stem)) {
|
|
2407
|
+
usedStems.add(stem);
|
|
2408
|
+
diverse.push(entry);
|
|
2409
|
+
} else {
|
|
2410
|
+
deferred.push(entry);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
shuffleInPlace(diverse);
|
|
2414
|
+
shuffleInPlace(deferred);
|
|
2415
|
+
return [...diverse, ...deferred].slice(0, limit).map((entry) => entry.cardId);
|
|
2416
|
+
}
|
|
2052
2417
|
resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
|
|
2053
2418
|
const supportTags = /* @__PURE__ */ new Set();
|
|
2054
2419
|
let blocked = false;
|
|
@@ -2094,7 +2459,6 @@ var init_prescribed = __esm({
|
|
|
2094
2459
|
}
|
|
2095
2460
|
collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
|
|
2096
2461
|
if (depth < 0 || visited.has(tag)) return;
|
|
2097
|
-
if (this.isHardGatedTag(tag)) return;
|
|
2098
2462
|
visited.add(tag);
|
|
2099
2463
|
let walkedFurther = false;
|
|
2100
2464
|
for (const hierarchy of hierarchyConfigs) {
|
|
@@ -2122,9 +2486,6 @@ var init_prescribed = __esm({
|
|
|
2122
2486
|
out.add(tag);
|
|
2123
2487
|
}
|
|
2124
2488
|
}
|
|
2125
|
-
isHardGatedTag(tag) {
|
|
2126
|
-
return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
|
|
2127
|
-
}
|
|
2128
2489
|
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
2129
2490
|
if (!userTagElo) return false;
|
|
2130
2491
|
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
|
|
@@ -3916,6 +4277,32 @@ var init_Pipeline = __esm({
|
|
|
3916
4277
|
cards = await this.hydrateTags(cards);
|
|
3917
4278
|
const tHydrate = performance.now();
|
|
3918
4279
|
const allCardsBeforeFiltering = [...cards];
|
|
4280
|
+
const pendingHints = this._ephemeralHints;
|
|
4281
|
+
if (pendingHints?.requireCards?.length) {
|
|
4282
|
+
const poolIds = new Set(allCardsBeforeFiltering.map((c) => c.cardId));
|
|
4283
|
+
const missingIds = pendingHints.requireCards.filter(
|
|
4284
|
+
(p) => !p.includes("*") && !poolIds.has(p)
|
|
4285
|
+
);
|
|
4286
|
+
if (missingIds.length > 0) {
|
|
4287
|
+
const fetchedTags = await this.course.getAppliedTagsBatch(missingIds);
|
|
4288
|
+
const courseId = this.course.getCourseID();
|
|
4289
|
+
for (const cardId of missingIds) {
|
|
4290
|
+
allCardsBeforeFiltering.push({
|
|
4291
|
+
cardId,
|
|
4292
|
+
courseId,
|
|
4293
|
+
score: 1,
|
|
4294
|
+
tags: fetchedTags.get(cardId) ?? [],
|
|
4295
|
+
provenance: []
|
|
4296
|
+
});
|
|
4297
|
+
}
|
|
4298
|
+
logger.info(
|
|
4299
|
+
`[Pipeline] Pre-fetched ${missingIds.length} required card(s) into pool: ${missingIds.join(", ")}`
|
|
4300
|
+
);
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
const prescribedIds = new Set(
|
|
4304
|
+
cards.filter((c) => c.provenance.some((p) => p.strategy === "prescribed")).map((c) => c.cardId)
|
|
4305
|
+
);
|
|
3919
4306
|
const filterImpacts = [];
|
|
3920
4307
|
for (const filter of this.filters) {
|
|
3921
4308
|
const beforeCount = cards.length;
|
|
@@ -3930,6 +4317,17 @@ var init_Pipeline = __esm({
|
|
|
3930
4317
|
else passed++;
|
|
3931
4318
|
}
|
|
3932
4319
|
filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
|
|
4320
|
+
if (prescribedIds.size > 0) {
|
|
4321
|
+
const survivingIds = new Set(cards.map((c) => c.cardId));
|
|
4322
|
+
const killedPrescribed = [...prescribedIds].filter((id) => !survivingIds.has(id));
|
|
4323
|
+
const zeroedPrescribed = cards.filter((c) => prescribedIds.has(c.cardId) && c.score === 0).map((c) => c.cardId);
|
|
4324
|
+
if (killedPrescribed.length > 0 || zeroedPrescribed.length > 0) {
|
|
4325
|
+
logger.info(
|
|
4326
|
+
`[Pipeline] Filter '${filter.name}' impact on prescribed cards: ` + (killedPrescribed.length > 0 ? `removed=[${killedPrescribed.join(", ")}] ` : "") + (zeroedPrescribed.length > 0 ? `zeroed=[${zeroedPrescribed.join(", ")}]` : "")
|
|
4327
|
+
);
|
|
4328
|
+
killedPrescribed.forEach((id) => prescribedIds.delete(id));
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
3933
4331
|
logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
|
|
3934
4332
|
}
|
|
3935
4333
|
cards = cards.filter((c) => c.score > 0);
|
|
@@ -3966,7 +4364,8 @@ var init_Pipeline = __esm({
|
|
|
3966
4364
|
filterImpacts,
|
|
3967
4365
|
cards,
|
|
3968
4366
|
result,
|
|
3969
|
-
context.userElo
|
|
4367
|
+
context.userElo,
|
|
4368
|
+
hints ?? void 0
|
|
3970
4369
|
);
|
|
3971
4370
|
captureRun(report);
|
|
3972
4371
|
} catch (e) {
|
|
@@ -4076,13 +4475,27 @@ var init_Pipeline = __esm({
|
|
|
4076
4475
|
}
|
|
4077
4476
|
}
|
|
4078
4477
|
const cardIds = new Set(cards.map((c) => c.cardId));
|
|
4478
|
+
const cardMap = new Map(cards.map((c) => [c.cardId, c]));
|
|
4079
4479
|
const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
|
|
4080
|
-
const
|
|
4081
|
-
|
|
4082
|
-
|
|
4480
|
+
const applyRequirement = (card, reason) => {
|
|
4481
|
+
const mandatoryScore = Number.POSITIVE_INFINITY;
|
|
4482
|
+
const existing = cardMap.get(card.cardId);
|
|
4483
|
+
if (existing) {
|
|
4484
|
+
if (existing.score < mandatoryScore) {
|
|
4485
|
+
existing.score = mandatoryScore;
|
|
4486
|
+
existing.provenance.push({
|
|
4487
|
+
strategy: "ephemeralHint",
|
|
4488
|
+
strategyId: "ephemeral-hint",
|
|
4489
|
+
strategyName: hintLabel,
|
|
4490
|
+
action: "boosted",
|
|
4491
|
+
score: mandatoryScore,
|
|
4492
|
+
reason: `${reason} (upgrade to mandatory score)`
|
|
4493
|
+
});
|
|
4494
|
+
}
|
|
4495
|
+
} else {
|
|
4083
4496
|
cards.push({
|
|
4084
4497
|
...card,
|
|
4085
|
-
score:
|
|
4498
|
+
score: mandatoryScore,
|
|
4086
4499
|
provenance: [
|
|
4087
4500
|
...card.provenance,
|
|
4088
4501
|
{
|
|
@@ -4090,25 +4503,41 @@ var init_Pipeline = __esm({
|
|
|
4090
4503
|
strategyId: "ephemeral-hint",
|
|
4091
4504
|
strategyName: hintLabel,
|
|
4092
4505
|
action: "boosted",
|
|
4093
|
-
score:
|
|
4506
|
+
score: mandatoryScore,
|
|
4094
4507
|
reason
|
|
4095
4508
|
}
|
|
4096
4509
|
]
|
|
4097
4510
|
});
|
|
4098
4511
|
cardIds.add(card.cardId);
|
|
4512
|
+
cardMap.set(card.cardId, cards[cards.length - 1]);
|
|
4099
4513
|
}
|
|
4100
4514
|
};
|
|
4101
4515
|
if (hints.requireCards?.length) {
|
|
4102
4516
|
for (const pattern of hints.requireCards) {
|
|
4517
|
+
for (const cardId of cardIds) {
|
|
4518
|
+
if (globMatch(cardId, pattern)) {
|
|
4519
|
+
applyRequirement(cardMap.get(cardId), `requireCard ${pattern}`);
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4103
4522
|
for (const card of allCards) {
|
|
4104
|
-
if (globMatch(card.cardId, pattern))
|
|
4523
|
+
if (globMatch(card.cardId, pattern)) {
|
|
4524
|
+
applyRequirement(card, `requireCard ${pattern}`);
|
|
4525
|
+
}
|
|
4105
4526
|
}
|
|
4106
4527
|
}
|
|
4107
4528
|
}
|
|
4108
4529
|
if (hints.requireTags?.length) {
|
|
4109
4530
|
for (const pattern of hints.requireTags) {
|
|
4531
|
+
for (const cardId of cardIds) {
|
|
4532
|
+
const card = cardMap.get(cardId);
|
|
4533
|
+
if (cardMatchesTagPattern(card, pattern)) {
|
|
4534
|
+
applyRequirement(card, `requireTag ${pattern}`);
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4110
4537
|
for (const card of allCards) {
|
|
4111
|
-
if (cardMatchesTagPattern(card, pattern))
|
|
4538
|
+
if (cardMatchesTagPattern(card, pattern)) {
|
|
4539
|
+
applyRequirement(card, `requireTag ${pattern}`);
|
|
4540
|
+
}
|
|
4112
4541
|
}
|
|
4113
4542
|
}
|
|
4114
4543
|
}
|
|
@@ -5262,7 +5691,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
5262
5691
|
}
|
|
5263
5692
|
}
|
|
5264
5693
|
async getCourseDoc(id, options) {
|
|
5265
|
-
return await this.db.get(id, options);
|
|
5694
|
+
return await this.db.get(id, options ?? {});
|
|
5266
5695
|
}
|
|
5267
5696
|
async getCourseDocs(ids, options = {}) {
|
|
5268
5697
|
return await this.db.allDocs({
|
|
@@ -5346,7 +5775,9 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
5346
5775
|
);
|
|
5347
5776
|
return pipeline;
|
|
5348
5777
|
} catch (e) {
|
|
5349
|
-
|
|
5778
|
+
const msg = e instanceof Error ? `${e.message}
|
|
5779
|
+
${e.stack}` : JSON.stringify(e);
|
|
5780
|
+
logger.error(`[courseDB] Error creating navigator: ${msg}`);
|
|
5350
5781
|
throw e;
|
|
5351
5782
|
}
|
|
5352
5783
|
}
|
|
@@ -10519,7 +10950,10 @@ var CardHydrationService = class {
|
|
|
10519
10950
|
this.hydrationInFlight.add(item.cardID);
|
|
10520
10951
|
try {
|
|
10521
10952
|
const courseDB = this.getCourseDB(item.courseID);
|
|
10522
|
-
const cardData = await
|
|
10953
|
+
const [cardData, tagsByCard] = await Promise.all([
|
|
10954
|
+
courseDB.getCourseDoc(item.cardID),
|
|
10955
|
+
courseDB.getAppliedTagsBatch([item.cardID])
|
|
10956
|
+
]);
|
|
10523
10957
|
if (!(0, import_common25.isCourseElo)(cardData.elo)) {
|
|
10524
10958
|
cardData.elo = (0, import_common25.toCourseElo)(cardData.elo);
|
|
10525
10959
|
}
|
|
@@ -10549,7 +10983,8 @@ var CardHydrationService = class {
|
|
|
10549
10983
|
this.hydratedCards.set(item.cardID, {
|
|
10550
10984
|
item,
|
|
10551
10985
|
view,
|
|
10552
|
-
data
|
|
10986
|
+
data,
|
|
10987
|
+
tags: tagsByCard.get(item.cardID) ?? []
|
|
10553
10988
|
});
|
|
10554
10989
|
logger.debug(`[CardHydrationService] Hydrated card ${item.cardID}`);
|
|
10555
10990
|
} finally {
|