@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/core/index.mjs
CHANGED
|
@@ -712,8 +712,9 @@ function getOrigin(card) {
|
|
|
712
712
|
const firstEntry = card.provenance[0];
|
|
713
713
|
if (!firstEntry) return "unknown";
|
|
714
714
|
const reason = firstEntry.reason?.toLowerCase() || "";
|
|
715
|
-
|
|
716
|
-
if (reason.includes("
|
|
715
|
+
const strategy = firstEntry.strategy?.toLowerCase() || "";
|
|
716
|
+
if (reason.includes("new card") || strategy.includes("elo")) return "new";
|
|
717
|
+
if (reason.includes("review") || strategy.includes("srs")) return "review";
|
|
717
718
|
return "unknown";
|
|
718
719
|
}
|
|
719
720
|
function captureRun(report) {
|
|
@@ -733,12 +734,13 @@ function parseCardElo(provenance) {
|
|
|
733
734
|
const match = eloEntry.reason.match(/card:\s*(\d+)/);
|
|
734
735
|
return match ? parseInt(match[1], 10) : void 0;
|
|
735
736
|
}
|
|
736
|
-
function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
|
|
737
|
+
function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
|
|
737
738
|
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
738
739
|
const cards = allCards.map((card) => ({
|
|
739
740
|
cardId: card.cardId,
|
|
740
741
|
courseId: card.courseId,
|
|
741
742
|
origin: getOrigin(card),
|
|
743
|
+
generator: card.provenance[0]?.strategyName || card.provenance[0]?.strategy,
|
|
742
744
|
finalScore: card.score,
|
|
743
745
|
cardElo: parseCardElo(card.provenance),
|
|
744
746
|
provenance: card.provenance,
|
|
@@ -755,6 +757,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
755
757
|
generators,
|
|
756
758
|
generatedCount,
|
|
757
759
|
filters,
|
|
760
|
+
hints,
|
|
758
761
|
finalCount: selectedCards.length,
|
|
759
762
|
reviewsSelected,
|
|
760
763
|
newSelected,
|
|
@@ -794,13 +797,169 @@ function printRunSummary(run) {
|
|
|
794
797
|
);
|
|
795
798
|
console.groupEnd();
|
|
796
799
|
}
|
|
800
|
+
function renderUI() {
|
|
801
|
+
if (!_uiContainer) return;
|
|
802
|
+
const runs = runHistory;
|
|
803
|
+
const selectedRun = _selectedRunIndex !== null ? runs[_selectedRunIndex] : null;
|
|
804
|
+
const styles = `
|
|
805
|
+
#sk-pipeline-debugger {
|
|
806
|
+
position: fixed;
|
|
807
|
+
top: 0;
|
|
808
|
+
left: 0;
|
|
809
|
+
width: 100vw;
|
|
810
|
+
height: 100vh;
|
|
811
|
+
background: #f8f9fa;
|
|
812
|
+
color: #212529;
|
|
813
|
+
z-index: 999999;
|
|
814
|
+
display: flex;
|
|
815
|
+
flex-direction: column;
|
|
816
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
817
|
+
font-size: 14px;
|
|
818
|
+
}
|
|
819
|
+
#sk-pipeline-debugger header {
|
|
820
|
+
padding: 1rem;
|
|
821
|
+
background: #343a40;
|
|
822
|
+
color: white;
|
|
823
|
+
display: flex;
|
|
824
|
+
justify-content: space-between;
|
|
825
|
+
align-items: center;
|
|
826
|
+
}
|
|
827
|
+
#sk-pipeline-debugger .container {
|
|
828
|
+
display: flex;
|
|
829
|
+
flex: 1;
|
|
830
|
+
overflow: hidden;
|
|
831
|
+
}
|
|
832
|
+
#sk-pipeline-debugger .sidebar {
|
|
833
|
+
width: 300px;
|
|
834
|
+
border-right: 1px solid #dee2e6;
|
|
835
|
+
overflow-y: auto;
|
|
836
|
+
background: white;
|
|
837
|
+
}
|
|
838
|
+
#sk-pipeline-debugger .main-content {
|
|
839
|
+
flex: 1;
|
|
840
|
+
overflow-y: auto;
|
|
841
|
+
padding: 1.5rem;
|
|
842
|
+
}
|
|
843
|
+
#sk-pipeline-debugger .run-item {
|
|
844
|
+
padding: 0.75rem 1rem;
|
|
845
|
+
border-bottom: 1px solid #eee;
|
|
846
|
+
cursor: pointer;
|
|
847
|
+
}
|
|
848
|
+
#sk-pipeline-debugger .run-item:hover { background: #f1f3f5; }
|
|
849
|
+
#sk-pipeline-debugger .run-item.active { background: #e9ecef; border-left: 4px solid #007bff; }
|
|
850
|
+
#sk-pipeline-debugger h2, #sk-pipeline-debugger h3 { margin-top: 0; }
|
|
851
|
+
#sk-pipeline-debugger table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; background: white; }
|
|
852
|
+
#sk-pipeline-debugger th, #sk-pipeline-debugger td { border: 1px solid #dee2e6; padding: 0.5rem; text-align: left; }
|
|
853
|
+
#sk-pipeline-debugger th { background: #f1f3f5; }
|
|
854
|
+
#sk-pipeline-debugger code { background: #f1f3f5; padding: 0.1rem 0.3rem; border-radius: 3px; font-family: monospace; }
|
|
855
|
+
#sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
|
|
856
|
+
#sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
|
|
857
|
+
#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; }
|
|
858
|
+
`;
|
|
859
|
+
const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
|
|
860
|
+
(r, i) => `
|
|
861
|
+
<div class="run-item ${i === _selectedRunIndex ? "active" : ""}" onclick="window.skuilder.pipeline._selectRun(${i})">
|
|
862
|
+
<strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
|
|
863
|
+
<small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
|
|
864
|
+
<small>${r.finalCount} cards selected</small>
|
|
865
|
+
</div>
|
|
866
|
+
`
|
|
867
|
+
).join("");
|
|
868
|
+
let detailsHtml = '<div style="color: #6c757d; text-align: center; margin-top: 5rem;">Select a run to see details</div>';
|
|
869
|
+
if (selectedRun) {
|
|
870
|
+
const filteredCards = selectedRun.cards.filter(
|
|
871
|
+
(c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
|
|
872
|
+
);
|
|
873
|
+
detailsHtml = `
|
|
874
|
+
<h2>Run: ${selectedRun.runId}</h2>
|
|
875
|
+
<p>
|
|
876
|
+
<strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
|
|
877
|
+
<strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
|
|
878
|
+
<strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
|
|
879
|
+
</p>
|
|
880
|
+
|
|
881
|
+
<h3>Pipeline Config</h3>
|
|
882
|
+
<table>
|
|
883
|
+
<tr><th>Generator</th><td>${selectedRun.generatorName} (${selectedRun.generatedCount} candidates)</td></tr>
|
|
884
|
+
${(selectedRun.generators || []).map(
|
|
885
|
+
(g) => `
|
|
886
|
+
<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>
|
|
887
|
+
`
|
|
888
|
+
).join("")}
|
|
889
|
+
</table>
|
|
890
|
+
|
|
891
|
+
${selectedRun.hints ? `
|
|
892
|
+
<h3>Ephemeral Hints</h3>
|
|
893
|
+
<table>
|
|
894
|
+
${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
|
|
895
|
+
${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
|
|
896
|
+
${selectedRun.hints.boostCards ? `<tr><th>Boost Cards</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostCards, null, 2)}</pre></td></tr>` : ""}
|
|
897
|
+
${selectedRun.hints.requireTags ? `<tr><th>Require Tags</th><td>${selectedRun.hints.requireTags.join(", ")}</td></tr>` : ""}
|
|
898
|
+
${selectedRun.hints.requireCards ? `<tr><th>Require Cards</th><td>${selectedRun.hints.requireCards.join(", ")}</td></tr>` : ""}
|
|
899
|
+
${selectedRun.hints.excludeTags ? `<tr><th>Exclude Tags</th><td>${selectedRun.hints.excludeTags.join(", ")}</td></tr>` : ""}
|
|
900
|
+
${selectedRun.hints.excludeCards ? `<tr><th>Exclude Cards</th><td>${selectedRun.hints.excludeCards.join(", ")}</td></tr>` : ""}
|
|
901
|
+
</table>
|
|
902
|
+
` : ""}
|
|
903
|
+
|
|
904
|
+
<h3>Filter Impact</h3>
|
|
905
|
+
<table>
|
|
906
|
+
<thead><tr><th>Filter</th><th>Boosted</th><th>Penalized</th><th>Passed</th><th>Removed</th></tr></thead>
|
|
907
|
+
<tbody>
|
|
908
|
+
${selectedRun.filters.map(
|
|
909
|
+
(f) => `
|
|
910
|
+
<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>
|
|
911
|
+
`
|
|
912
|
+
).join("")}
|
|
913
|
+
</tbody>
|
|
914
|
+
</table>
|
|
915
|
+
|
|
916
|
+
<h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
|
|
917
|
+
<input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
|
|
918
|
+
|
|
919
|
+
<table>
|
|
920
|
+
<thead><tr><th>ID</th><th>Generator</th><th>Origin</th><th>Score</th><th>Selected</th></tr></thead>
|
|
921
|
+
<tbody>
|
|
922
|
+
${filteredCards.map(
|
|
923
|
+
(c) => `
|
|
924
|
+
<tr>
|
|
925
|
+
<td><code>${c.cardId}</code></td>
|
|
926
|
+
<td>${c.generator || "unknown"}</td>
|
|
927
|
+
<td>${c.origin}</td>
|
|
928
|
+
<td>${c.finalScore.toFixed(3)}</td>
|
|
929
|
+
<td>${c.selected ? "\u2705" : "\u274C"}</td>
|
|
930
|
+
</tr>
|
|
931
|
+
${c.selected || _cardSearchQuery ? `
|
|
932
|
+
<tr>
|
|
933
|
+
<td colspan="5">
|
|
934
|
+
<div class="provenance">${formatProvenance(c.provenance)}</div>
|
|
935
|
+
</td>
|
|
936
|
+
</tr>
|
|
937
|
+
` : ""}
|
|
938
|
+
`
|
|
939
|
+
).join("")}
|
|
940
|
+
</tbody>
|
|
941
|
+
</table>
|
|
942
|
+
`;
|
|
943
|
+
}
|
|
944
|
+
_uiContainer.innerHTML = `
|
|
945
|
+
<style>${styles}</style>
|
|
946
|
+
<header>
|
|
947
|
+
<strong>Pipeline Debugger</strong>
|
|
948
|
+
<button class="close-btn" onclick="window.skuilder.pipeline.ui()">Close</button>
|
|
949
|
+
</header>
|
|
950
|
+
<div class="container">
|
|
951
|
+
<div class="sidebar">${runListHtml}</div>
|
|
952
|
+
<div class="main-content">${detailsHtml}</div>
|
|
953
|
+
</div>
|
|
954
|
+
`;
|
|
955
|
+
}
|
|
797
956
|
function mountPipelineDebugger() {
|
|
798
957
|
if (typeof window === "undefined") return;
|
|
799
958
|
const win = window;
|
|
800
959
|
win.skuilder = win.skuilder || {};
|
|
801
960
|
win.skuilder.pipeline = pipelineDebugAPI;
|
|
802
961
|
}
|
|
803
|
-
var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
|
|
962
|
+
var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
|
|
804
963
|
var init_PipelineDebugger = __esm({
|
|
805
964
|
"src/core/navigators/PipelineDebugger.ts"() {
|
|
806
965
|
"use strict";
|
|
@@ -809,6 +968,9 @@ var init_PipelineDebugger = __esm({
|
|
|
809
968
|
_activePipeline = null;
|
|
810
969
|
MAX_RUNS = 10;
|
|
811
970
|
runHistory = [];
|
|
971
|
+
_uiContainer = null;
|
|
972
|
+
_selectedRunIndex = null;
|
|
973
|
+
_cardSearchQuery = "";
|
|
812
974
|
pipelineDebugAPI = {
|
|
813
975
|
/**
|
|
814
976
|
* Get raw run history for programmatic access.
|
|
@@ -948,16 +1110,20 @@ var init_PipelineDebugger = __esm({
|
|
|
948
1110
|
const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
|
|
949
1111
|
const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
|
|
950
1112
|
const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
|
|
1113
|
+
const supportCard = reason.match(/supportCard=([^;]+)/)?.[1] ?? "none";
|
|
951
1114
|
const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
|
|
952
1115
|
const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
|
|
1116
|
+
const supportSource = mode === "discovered-support" ? "discovered" : mode === "support" ? "authored" : "n/a";
|
|
953
1117
|
return {
|
|
954
1118
|
group: parsedGroup,
|
|
955
1119
|
mode,
|
|
1120
|
+
supportSource,
|
|
956
1121
|
cardId: card.cardId,
|
|
957
1122
|
selected: card.selected ? "yes" : "no",
|
|
958
1123
|
finalScore: card.finalScore.toFixed(3),
|
|
959
1124
|
blocked,
|
|
960
1125
|
blockedTargets,
|
|
1126
|
+
supportCard,
|
|
961
1127
|
supportTags,
|
|
962
1128
|
multiplier
|
|
963
1129
|
};
|
|
@@ -973,6 +1139,8 @@ var init_PipelineDebugger = __esm({
|
|
|
973
1139
|
const selectedRows = rows.filter((r) => r.selected === "yes");
|
|
974
1140
|
const blockedTargetSet = /* @__PURE__ */ new Set();
|
|
975
1141
|
const supportTagSet = /* @__PURE__ */ new Set();
|
|
1142
|
+
const authoredSupportSet = /* @__PURE__ */ new Set();
|
|
1143
|
+
const discoveredSupportSet = /* @__PURE__ */ new Set();
|
|
976
1144
|
for (const row of rows) {
|
|
977
1145
|
if (row.blockedTargets && row.blockedTargets !== "none") {
|
|
978
1146
|
row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
|
|
@@ -980,6 +1148,13 @@ var init_PipelineDebugger = __esm({
|
|
|
980
1148
|
if (row.supportTags && row.supportTags !== "none") {
|
|
981
1149
|
row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
|
|
982
1150
|
}
|
|
1151
|
+
if (row.supportCard && row.supportCard !== "none") {
|
|
1152
|
+
if (row.supportSource === "discovered") {
|
|
1153
|
+
discoveredSupportSet.add(row.supportCard);
|
|
1154
|
+
} else if (row.supportSource === "authored") {
|
|
1155
|
+
authoredSupportSet.add(row.supportCard);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
983
1158
|
}
|
|
984
1159
|
logger.info(`Prescribed cards in run: ${rows.length}`);
|
|
985
1160
|
logger.info(`Selected prescribed cards: ${selectedRows.length}`);
|
|
@@ -989,6 +1164,12 @@ var init_PipelineDebugger = __esm({
|
|
|
989
1164
|
logger.info(
|
|
990
1165
|
`Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
|
|
991
1166
|
);
|
|
1167
|
+
logger.info(
|
|
1168
|
+
`Authored support cards emitted: ${authoredSupportSet.size > 0 ? [...authoredSupportSet].join(", ") : "none"}`
|
|
1169
|
+
);
|
|
1170
|
+
logger.info(
|
|
1171
|
+
`Discovered support cards emitted: ${discoveredSupportSet.size > 0 ? [...discoveredSupportSet].join(", ") : "none"}`
|
|
1172
|
+
);
|
|
992
1173
|
console.groupEnd();
|
|
993
1174
|
},
|
|
994
1175
|
/**
|
|
@@ -1123,6 +1304,39 @@ var init_PipelineDebugger = __esm({
|
|
|
1123
1304
|
Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
|
|
1124
1305
|
);
|
|
1125
1306
|
},
|
|
1307
|
+
/**
|
|
1308
|
+
* Toggle the full-screen UI debugger.
|
|
1309
|
+
*/
|
|
1310
|
+
ui() {
|
|
1311
|
+
if (_uiContainer) {
|
|
1312
|
+
document.body.removeChild(_uiContainer);
|
|
1313
|
+
_uiContainer = null;
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
_uiContainer = document.createElement("div");
|
|
1317
|
+
_uiContainer.id = "sk-pipeline-debugger";
|
|
1318
|
+
document.body.appendChild(_uiContainer);
|
|
1319
|
+
if (_selectedRunIndex === null && runHistory.length > 0) {
|
|
1320
|
+
_selectedRunIndex = 0;
|
|
1321
|
+
}
|
|
1322
|
+
renderUI();
|
|
1323
|
+
},
|
|
1324
|
+
/**
|
|
1325
|
+
* Internal UI helpers
|
|
1326
|
+
* @internal
|
|
1327
|
+
*/
|
|
1328
|
+
_selectRun(index) {
|
|
1329
|
+
_selectedRunIndex = index;
|
|
1330
|
+
renderUI();
|
|
1331
|
+
},
|
|
1332
|
+
/**
|
|
1333
|
+
* Internal UI helpers
|
|
1334
|
+
* @internal
|
|
1335
|
+
*/
|
|
1336
|
+
_setSearch(query) {
|
|
1337
|
+
_cardSearchQuery = query;
|
|
1338
|
+
renderUI();
|
|
1339
|
+
},
|
|
1126
1340
|
/**
|
|
1127
1341
|
* Show help.
|
|
1128
1342
|
*/
|
|
@@ -1131,6 +1345,7 @@ var init_PipelineDebugger = __esm({
|
|
|
1131
1345
|
\u{1F527} Pipeline Debug API
|
|
1132
1346
|
|
|
1133
1347
|
Commands:
|
|
1348
|
+
.ui() Toggle full-screen UI debugger
|
|
1134
1349
|
.showLastRun() Show summary of most recent pipeline run
|
|
1135
1350
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
1136
1351
|
.showCard(cardId) Show provenance trail for a specific card
|
|
@@ -1147,6 +1362,7 @@ Commands:
|
|
|
1147
1362
|
.help() Show this help message
|
|
1148
1363
|
|
|
1149
1364
|
Example:
|
|
1365
|
+
window.skuilder.pipeline.ui()
|
|
1150
1366
|
window.skuilder.pipeline.showLastRun()
|
|
1151
1367
|
window.skuilder.pipeline.showRun(1)
|
|
1152
1368
|
await window.skuilder.pipeline.diagnoseCardSpace()
|
|
@@ -1309,7 +1525,7 @@ var init_CompositeGenerator = __esm({
|
|
|
1309
1525
|
for (const [, items] of byCardId) {
|
|
1310
1526
|
const cards2 = items.map((i) => i.card);
|
|
1311
1527
|
const aggregatedScore = this.aggregateScores(items);
|
|
1312
|
-
const finalScore = Math.
|
|
1528
|
+
const finalScore = Math.max(0, aggregatedScore);
|
|
1313
1529
|
const mergedProvenance = cards2.flatMap((c) => c.provenance);
|
|
1314
1530
|
const initialScore = cards2[0].score;
|
|
1315
1531
|
const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
|
|
@@ -1506,10 +1722,26 @@ function matchesTagPattern(tag, pattern) {
|
|
|
1506
1722
|
const re = new RegExp(`^${escaped}$`);
|
|
1507
1723
|
return re.test(tag);
|
|
1508
1724
|
}
|
|
1725
|
+
function extractWordStem(cardId) {
|
|
1726
|
+
for (const prefix of ["c-ml-", "c-ws-", "c-spelling-"]) {
|
|
1727
|
+
if (cardId.startsWith(prefix)) {
|
|
1728
|
+
const rest = cardId.slice(prefix.length);
|
|
1729
|
+
const lastDash = rest.lastIndexOf("-");
|
|
1730
|
+
return lastDash > 0 ? rest.slice(0, lastDash) : rest;
|
|
1731
|
+
}
|
|
1732
|
+
}
|
|
1733
|
+
return cardId;
|
|
1734
|
+
}
|
|
1735
|
+
function shuffleInPlace(arr) {
|
|
1736
|
+
for (let i = arr.length - 1; i > 0; i--) {
|
|
1737
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1738
|
+
[arr[i], arr[j]] = [arr[j], arr[i]];
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1509
1741
|
function pickTopByScore(cards, limit) {
|
|
1510
1742
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1511
1743
|
}
|
|
1512
|
-
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,
|
|
1744
|
+
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;
|
|
1513
1745
|
var init_prescribed = __esm({
|
|
1514
1746
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1515
1747
|
"use strict";
|
|
@@ -1522,11 +1754,10 @@ var init_prescribed = __esm({
|
|
|
1522
1754
|
DEFAULT_MIN_COUNT = 3;
|
|
1523
1755
|
BASE_TARGET_SCORE = 1;
|
|
1524
1756
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1757
|
+
DISCOVERED_SUPPORT_SCORE = 12;
|
|
1525
1758
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1526
1759
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1527
|
-
|
|
1528
|
-
LESSON_GATE_PENALTY_TAG_HINT = "concept:";
|
|
1529
|
-
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
|
|
1760
|
+
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
1530
1761
|
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1531
1762
|
name;
|
|
1532
1763
|
config;
|
|
@@ -1562,6 +1793,20 @@ var init_prescribed = __esm({
|
|
|
1562
1793
|
const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
|
|
1563
1794
|
const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
|
|
1564
1795
|
const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
|
|
1796
|
+
const courseTagDocs = await this.course.getCourseTagStubs().catch(
|
|
1797
|
+
() => ({
|
|
1798
|
+
rows: [],
|
|
1799
|
+
offset: 0,
|
|
1800
|
+
total_rows: 0
|
|
1801
|
+
})
|
|
1802
|
+
);
|
|
1803
|
+
const cardsByTag = /* @__PURE__ */ new Map();
|
|
1804
|
+
for (const row of courseTagDocs.rows ?? []) {
|
|
1805
|
+
const tagDoc = row.doc;
|
|
1806
|
+
if (tagDoc?.name && Array.isArray(tagDoc.taggedCards)) {
|
|
1807
|
+
cardsByTag.set(tagDoc.name, [...tagDoc.taggedCards]);
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1565
1810
|
const nextState = {
|
|
1566
1811
|
updatedAt: isoNow(),
|
|
1567
1812
|
groups: {}
|
|
@@ -1576,11 +1821,31 @@ var init_prescribed = __esm({
|
|
|
1576
1821
|
activeIds,
|
|
1577
1822
|
seenIds,
|
|
1578
1823
|
tagsByCard,
|
|
1824
|
+
cardsByTag,
|
|
1579
1825
|
hierarchyConfigs,
|
|
1580
1826
|
userTagElo,
|
|
1581
1827
|
userGlobalElo
|
|
1582
1828
|
});
|
|
1583
1829
|
groupRuntimes.push(runtime);
|
|
1830
|
+
logger.info(
|
|
1831
|
+
`[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)}`
|
|
1832
|
+
);
|
|
1833
|
+
if (runtime.blockedTargets.length > 0) {
|
|
1834
|
+
logger.info(
|
|
1835
|
+
`[Prescribed] Group '${group.id}' blocked targets: ${runtime.blockedTargets.join(", ")}`
|
|
1836
|
+
);
|
|
1837
|
+
logger.info(
|
|
1838
|
+
`[Prescribed] Group '${group.id}' support tags needed: ${runtime.supportTags.join(", ") || "(none)"}`
|
|
1839
|
+
);
|
|
1840
|
+
logger.info(
|
|
1841
|
+
`[Prescribed] Group '${group.id}' escalation mode: ` + (runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support-candidates" : "boost-only")
|
|
1842
|
+
);
|
|
1843
|
+
if (runtime.discoveredSupportCandidates.length > 0) {
|
|
1844
|
+
logger.info(
|
|
1845
|
+
`[Prescribed] Group '${group.id}' discovered support candidates: ${runtime.discoveredSupportCandidates.join(", ")}`
|
|
1846
|
+
);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1584
1849
|
nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
|
|
1585
1850
|
const directCards = this.buildDirectTargetCards(
|
|
1586
1851
|
runtime,
|
|
@@ -1592,15 +1857,30 @@ var init_prescribed = __esm({
|
|
|
1592
1857
|
courseId,
|
|
1593
1858
|
emittedIds
|
|
1594
1859
|
);
|
|
1595
|
-
|
|
1860
|
+
const discoveredSupportCards = this.buildDiscoveredSupportCards(
|
|
1861
|
+
runtime,
|
|
1862
|
+
courseId,
|
|
1863
|
+
emittedIds
|
|
1864
|
+
);
|
|
1865
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
|
|
1596
1866
|
}
|
|
1597
1867
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
1598
1868
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
1599
1869
|
boostTags: hintSummary.boostTags,
|
|
1600
1870
|
_label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
|
|
1601
1871
|
} : void 0;
|
|
1872
|
+
if (hints) {
|
|
1873
|
+
const tagEntries = Object.entries(hints.boostTags ?? {});
|
|
1874
|
+
logger.info(
|
|
1875
|
+
`[Prescribed] Emitting ${tagEntries.length} boost hint(s): ` + tagEntries.map(([tag, mult]) => `${tag}\xD7${mult.toFixed(1)}`).join(", ")
|
|
1876
|
+
);
|
|
1877
|
+
} else {
|
|
1878
|
+
logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
|
|
1879
|
+
}
|
|
1602
1880
|
if (emitted.length === 0) {
|
|
1603
|
-
logger.
|
|
1881
|
+
logger.info(
|
|
1882
|
+
"[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
|
|
1883
|
+
);
|
|
1604
1884
|
await this.putStrategyState(nextState).catch((e) => {
|
|
1605
1885
|
logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
|
|
1606
1886
|
});
|
|
@@ -1633,7 +1913,7 @@ var init_prescribed = __esm({
|
|
|
1633
1913
|
logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
|
|
1634
1914
|
});
|
|
1635
1915
|
logger.info(
|
|
1636
|
-
`[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)`
|
|
1916
|
+
`[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)`
|
|
1637
1917
|
);
|
|
1638
1918
|
return hints ? { cards: finalCards, hints } : { cards: finalCards };
|
|
1639
1919
|
}
|
|
@@ -1663,9 +1943,15 @@ var init_prescribed = __esm({
|
|
|
1663
1943
|
const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
|
|
1664
1944
|
const groups = groupsRaw.map((raw, i) => ({
|
|
1665
1945
|
id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
|
|
1666
|
-
targetCardIds: dedupe(
|
|
1667
|
-
|
|
1668
|
-
|
|
1946
|
+
targetCardIds: dedupe(
|
|
1947
|
+
Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []
|
|
1948
|
+
),
|
|
1949
|
+
supportCardIds: dedupe(
|
|
1950
|
+
Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []
|
|
1951
|
+
),
|
|
1952
|
+
supportTagPatterns: dedupe(
|
|
1953
|
+
Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []
|
|
1954
|
+
),
|
|
1669
1955
|
freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
|
|
1670
1956
|
maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
|
|
1671
1957
|
maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
|
|
@@ -1682,7 +1968,7 @@ var init_prescribed = __esm({
|
|
|
1682
1968
|
}
|
|
1683
1969
|
async loadHierarchyConfigs() {
|
|
1684
1970
|
try {
|
|
1685
|
-
const strategies = await this.course.
|
|
1971
|
+
const strategies = await this.course.getAllNavigationStrategies();
|
|
1686
1972
|
return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
|
|
1687
1973
|
try {
|
|
1688
1974
|
const parsed = JSON.parse(s.serializedData);
|
|
@@ -1705,6 +1991,7 @@ var init_prescribed = __esm({
|
|
|
1705
1991
|
activeIds,
|
|
1706
1992
|
seenIds,
|
|
1707
1993
|
tagsByCard,
|
|
1994
|
+
cardsByTag,
|
|
1708
1995
|
hierarchyConfigs,
|
|
1709
1996
|
userTagElo,
|
|
1710
1997
|
userGlobalElo
|
|
@@ -1768,6 +2055,22 @@ var init_prescribed = __esm({
|
|
|
1768
2055
|
[...supportTags]
|
|
1769
2056
|
)
|
|
1770
2057
|
]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
|
|
2058
|
+
const discoveredSupportCandidates = blockedTargets.length > 0 && supportTags.size > 0 && supportCandidates.length === 0 ? this.findDiscoveredSupportCards({
|
|
2059
|
+
supportTags: [...supportTags],
|
|
2060
|
+
cardsByTag,
|
|
2061
|
+
activeIds,
|
|
2062
|
+
seenIds,
|
|
2063
|
+
excludedIds: /* @__PURE__ */ new Set([
|
|
2064
|
+
...group.targetCardIds,
|
|
2065
|
+
...group.supportCardIds ?? []
|
|
2066
|
+
]),
|
|
2067
|
+
limit: group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN
|
|
2068
|
+
}) : [];
|
|
2069
|
+
if (blockedTargets.length > 0 && supportTags.size > 0 && discoveredSupportCandidates.length === 0) {
|
|
2070
|
+
logger.info(
|
|
2071
|
+
`[Prescribed] Group '${group.id}' discovered 0 broader support candidates (blocked=${blockedTargets.length}; authoredSupport=${supportCandidates.length})`
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
1771
2074
|
const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
|
|
1772
2075
|
const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
1773
2076
|
const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
|
|
@@ -1781,6 +2084,7 @@ var init_prescribed = __esm({
|
|
|
1781
2084
|
surfaceableTargets,
|
|
1782
2085
|
targetTags,
|
|
1783
2086
|
supportCandidates,
|
|
2087
|
+
discoveredSupportCandidates,
|
|
1784
2088
|
supportTags: [...supportTags],
|
|
1785
2089
|
pressureMultiplier,
|
|
1786
2090
|
supportMultiplier,
|
|
@@ -1851,6 +2155,33 @@ var init_prescribed = __esm({
|
|
|
1851
2155
|
}
|
|
1852
2156
|
return cards;
|
|
1853
2157
|
}
|
|
2158
|
+
buildDiscoveredSupportCards(runtime, courseId, emittedIds) {
|
|
2159
|
+
if (runtime.blockedTargets.length === 0 || runtime.discoveredSupportCandidates.length === 0) {
|
|
2160
|
+
return [];
|
|
2161
|
+
}
|
|
2162
|
+
const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
|
|
2163
|
+
const supportIds = runtime.discoveredSupportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
|
|
2164
|
+
const cards = [];
|
|
2165
|
+
for (const cardId of supportIds) {
|
|
2166
|
+
emittedIds.add(cardId);
|
|
2167
|
+
cards.push({
|
|
2168
|
+
cardId,
|
|
2169
|
+
courseId,
|
|
2170
|
+
score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
2171
|
+
provenance: [
|
|
2172
|
+
{
|
|
2173
|
+
strategy: "prescribed",
|
|
2174
|
+
strategyName: this.strategyName || this.name,
|
|
2175
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2176
|
+
action: "generated",
|
|
2177
|
+
score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
2178
|
+
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}`
|
|
2179
|
+
}
|
|
2180
|
+
]
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
return cards;
|
|
2184
|
+
}
|
|
1854
2185
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
1855
2186
|
if (supportTags.length === 0) {
|
|
1856
2187
|
return [];
|
|
@@ -1873,6 +2204,40 @@ var init_prescribed = __esm({
|
|
|
1873
2204
|
}
|
|
1874
2205
|
return [...candidates];
|
|
1875
2206
|
}
|
|
2207
|
+
findDiscoveredSupportCards(args) {
|
|
2208
|
+
const { supportTags, cardsByTag, activeIds, seenIds, excludedIds, limit } = args;
|
|
2209
|
+
const byCardId = /* @__PURE__ */ new Map();
|
|
2210
|
+
for (const supportTag of supportTags) {
|
|
2211
|
+
const taggedCards = cardsByTag.get(supportTag) ?? [];
|
|
2212
|
+
for (const cardId of taggedCards) {
|
|
2213
|
+
if (activeIds.has(cardId) || seenIds.has(cardId) || excludedIds.has(cardId)) {
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
2216
|
+
const existing = byCardId.get(cardId);
|
|
2217
|
+
if (existing) {
|
|
2218
|
+
existing.matches += 1;
|
|
2219
|
+
} else {
|
|
2220
|
+
byCardId.set(cardId, { cardId, matches: 1 });
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
}
|
|
2224
|
+
const candidates = [...byCardId.values()].sort((a, b) => b.matches - a.matches || a.cardId.localeCompare(b.cardId));
|
|
2225
|
+
const usedStems = /* @__PURE__ */ new Set();
|
|
2226
|
+
const diverse = [];
|
|
2227
|
+
const deferred = [];
|
|
2228
|
+
for (const entry of candidates) {
|
|
2229
|
+
const stem = extractWordStem(entry.cardId);
|
|
2230
|
+
if (!usedStems.has(stem)) {
|
|
2231
|
+
usedStems.add(stem);
|
|
2232
|
+
diverse.push(entry);
|
|
2233
|
+
} else {
|
|
2234
|
+
deferred.push(entry);
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
shuffleInPlace(diverse);
|
|
2238
|
+
shuffleInPlace(deferred);
|
|
2239
|
+
return [...diverse, ...deferred].slice(0, limit).map((entry) => entry.cardId);
|
|
2240
|
+
}
|
|
1876
2241
|
resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
|
|
1877
2242
|
const supportTags = /* @__PURE__ */ new Set();
|
|
1878
2243
|
let blocked = false;
|
|
@@ -1918,7 +2283,6 @@ var init_prescribed = __esm({
|
|
|
1918
2283
|
}
|
|
1919
2284
|
collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
|
|
1920
2285
|
if (depth < 0 || visited.has(tag)) return;
|
|
1921
|
-
if (this.isHardGatedTag(tag)) return;
|
|
1922
2286
|
visited.add(tag);
|
|
1923
2287
|
let walkedFurther = false;
|
|
1924
2288
|
for (const hierarchy of hierarchyConfigs) {
|
|
@@ -1946,9 +2310,6 @@ var init_prescribed = __esm({
|
|
|
1946
2310
|
out.add(tag);
|
|
1947
2311
|
}
|
|
1948
2312
|
}
|
|
1949
|
-
isHardGatedTag(tag) {
|
|
1950
|
-
return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
|
|
1951
|
-
}
|
|
1952
2313
|
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
1953
2314
|
if (!userTagElo) return false;
|
|
1954
2315
|
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
|
|
@@ -3740,6 +4101,32 @@ var init_Pipeline = __esm({
|
|
|
3740
4101
|
cards = await this.hydrateTags(cards);
|
|
3741
4102
|
const tHydrate = performance.now();
|
|
3742
4103
|
const allCardsBeforeFiltering = [...cards];
|
|
4104
|
+
const pendingHints = this._ephemeralHints;
|
|
4105
|
+
if (pendingHints?.requireCards?.length) {
|
|
4106
|
+
const poolIds = new Set(allCardsBeforeFiltering.map((c) => c.cardId));
|
|
4107
|
+
const missingIds = pendingHints.requireCards.filter(
|
|
4108
|
+
(p) => !p.includes("*") && !poolIds.has(p)
|
|
4109
|
+
);
|
|
4110
|
+
if (missingIds.length > 0) {
|
|
4111
|
+
const fetchedTags = await this.course.getAppliedTagsBatch(missingIds);
|
|
4112
|
+
const courseId = this.course.getCourseID();
|
|
4113
|
+
for (const cardId of missingIds) {
|
|
4114
|
+
allCardsBeforeFiltering.push({
|
|
4115
|
+
cardId,
|
|
4116
|
+
courseId,
|
|
4117
|
+
score: 1,
|
|
4118
|
+
tags: fetchedTags.get(cardId) ?? [],
|
|
4119
|
+
provenance: []
|
|
4120
|
+
});
|
|
4121
|
+
}
|
|
4122
|
+
logger.info(
|
|
4123
|
+
`[Pipeline] Pre-fetched ${missingIds.length} required card(s) into pool: ${missingIds.join(", ")}`
|
|
4124
|
+
);
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
const prescribedIds = new Set(
|
|
4128
|
+
cards.filter((c) => c.provenance.some((p) => p.strategy === "prescribed")).map((c) => c.cardId)
|
|
4129
|
+
);
|
|
3743
4130
|
const filterImpacts = [];
|
|
3744
4131
|
for (const filter of this.filters) {
|
|
3745
4132
|
const beforeCount = cards.length;
|
|
@@ -3754,6 +4141,17 @@ var init_Pipeline = __esm({
|
|
|
3754
4141
|
else passed++;
|
|
3755
4142
|
}
|
|
3756
4143
|
filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
|
|
4144
|
+
if (prescribedIds.size > 0) {
|
|
4145
|
+
const survivingIds = new Set(cards.map((c) => c.cardId));
|
|
4146
|
+
const killedPrescribed = [...prescribedIds].filter((id) => !survivingIds.has(id));
|
|
4147
|
+
const zeroedPrescribed = cards.filter((c) => prescribedIds.has(c.cardId) && c.score === 0).map((c) => c.cardId);
|
|
4148
|
+
if (killedPrescribed.length > 0 || zeroedPrescribed.length > 0) {
|
|
4149
|
+
logger.info(
|
|
4150
|
+
`[Pipeline] Filter '${filter.name}' impact on prescribed cards: ` + (killedPrescribed.length > 0 ? `removed=[${killedPrescribed.join(", ")}] ` : "") + (zeroedPrescribed.length > 0 ? `zeroed=[${zeroedPrescribed.join(", ")}]` : "")
|
|
4151
|
+
);
|
|
4152
|
+
killedPrescribed.forEach((id) => prescribedIds.delete(id));
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
3757
4155
|
logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
|
|
3758
4156
|
}
|
|
3759
4157
|
cards = cards.filter((c) => c.score > 0);
|
|
@@ -3790,7 +4188,8 @@ var init_Pipeline = __esm({
|
|
|
3790
4188
|
filterImpacts,
|
|
3791
4189
|
cards,
|
|
3792
4190
|
result,
|
|
3793
|
-
context.userElo
|
|
4191
|
+
context.userElo,
|
|
4192
|
+
hints ?? void 0
|
|
3794
4193
|
);
|
|
3795
4194
|
captureRun(report);
|
|
3796
4195
|
} catch (e) {
|
|
@@ -3900,13 +4299,27 @@ var init_Pipeline = __esm({
|
|
|
3900
4299
|
}
|
|
3901
4300
|
}
|
|
3902
4301
|
const cardIds = new Set(cards.map((c) => c.cardId));
|
|
4302
|
+
const cardMap = new Map(cards.map((c) => [c.cardId, c]));
|
|
3903
4303
|
const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
|
|
3904
|
-
const
|
|
3905
|
-
|
|
3906
|
-
|
|
4304
|
+
const applyRequirement = (card, reason) => {
|
|
4305
|
+
const mandatoryScore = Number.POSITIVE_INFINITY;
|
|
4306
|
+
const existing = cardMap.get(card.cardId);
|
|
4307
|
+
if (existing) {
|
|
4308
|
+
if (existing.score < mandatoryScore) {
|
|
4309
|
+
existing.score = mandatoryScore;
|
|
4310
|
+
existing.provenance.push({
|
|
4311
|
+
strategy: "ephemeralHint",
|
|
4312
|
+
strategyId: "ephemeral-hint",
|
|
4313
|
+
strategyName: hintLabel,
|
|
4314
|
+
action: "boosted",
|
|
4315
|
+
score: mandatoryScore,
|
|
4316
|
+
reason: `${reason} (upgrade to mandatory score)`
|
|
4317
|
+
});
|
|
4318
|
+
}
|
|
4319
|
+
} else {
|
|
3907
4320
|
cards.push({
|
|
3908
4321
|
...card,
|
|
3909
|
-
score:
|
|
4322
|
+
score: mandatoryScore,
|
|
3910
4323
|
provenance: [
|
|
3911
4324
|
...card.provenance,
|
|
3912
4325
|
{
|
|
@@ -3914,25 +4327,41 @@ var init_Pipeline = __esm({
|
|
|
3914
4327
|
strategyId: "ephemeral-hint",
|
|
3915
4328
|
strategyName: hintLabel,
|
|
3916
4329
|
action: "boosted",
|
|
3917
|
-
score:
|
|
4330
|
+
score: mandatoryScore,
|
|
3918
4331
|
reason
|
|
3919
4332
|
}
|
|
3920
4333
|
]
|
|
3921
4334
|
});
|
|
3922
4335
|
cardIds.add(card.cardId);
|
|
4336
|
+
cardMap.set(card.cardId, cards[cards.length - 1]);
|
|
3923
4337
|
}
|
|
3924
4338
|
};
|
|
3925
4339
|
if (hints.requireCards?.length) {
|
|
3926
4340
|
for (const pattern of hints.requireCards) {
|
|
4341
|
+
for (const cardId of cardIds) {
|
|
4342
|
+
if (globMatch(cardId, pattern)) {
|
|
4343
|
+
applyRequirement(cardMap.get(cardId), `requireCard ${pattern}`);
|
|
4344
|
+
}
|
|
4345
|
+
}
|
|
3927
4346
|
for (const card of allCards) {
|
|
3928
|
-
if (globMatch(card.cardId, pattern))
|
|
4347
|
+
if (globMatch(card.cardId, pattern)) {
|
|
4348
|
+
applyRequirement(card, `requireCard ${pattern}`);
|
|
4349
|
+
}
|
|
3929
4350
|
}
|
|
3930
4351
|
}
|
|
3931
4352
|
}
|
|
3932
4353
|
if (hints.requireTags?.length) {
|
|
3933
4354
|
for (const pattern of hints.requireTags) {
|
|
4355
|
+
for (const cardId of cardIds) {
|
|
4356
|
+
const card = cardMap.get(cardId);
|
|
4357
|
+
if (cardMatchesTagPattern(card, pattern)) {
|
|
4358
|
+
applyRequirement(card, `requireTag ${pattern}`);
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
3934
4361
|
for (const card of allCards) {
|
|
3935
|
-
if (cardMatchesTagPattern(card, pattern))
|
|
4362
|
+
if (cardMatchesTagPattern(card, pattern)) {
|
|
4363
|
+
applyRequirement(card, `requireTag ${pattern}`);
|
|
4364
|
+
}
|
|
3936
4365
|
}
|
|
3937
4366
|
}
|
|
3938
4367
|
}
|
|
@@ -5046,7 +5475,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
5046
5475
|
}
|
|
5047
5476
|
}
|
|
5048
5477
|
async getCourseDoc(id, options) {
|
|
5049
|
-
return await this.db.get(id, options);
|
|
5478
|
+
return await this.db.get(id, options ?? {});
|
|
5050
5479
|
}
|
|
5051
5480
|
async getCourseDocs(ids, options = {}) {
|
|
5052
5481
|
return await this.db.allDocs({
|
|
@@ -5130,7 +5559,9 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
5130
5559
|
);
|
|
5131
5560
|
return pipeline;
|
|
5132
5561
|
} catch (e) {
|
|
5133
|
-
|
|
5562
|
+
const msg = e instanceof Error ? `${e.message}
|
|
5563
|
+
${e.stack}` : JSON.stringify(e);
|
|
5564
|
+
logger.error(`[courseDB] Error creating navigator: ${msg}`);
|
|
5134
5565
|
throw e;
|
|
5135
5566
|
}
|
|
5136
5567
|
}
|