@vue-skuilder/db 0.1.34 → 0.1.36
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 +46 -0
- package/dist/core/index.d.ts +46 -0
- package/dist/core/index.js +163 -12
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +163 -12
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +163 -12
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +163 -12
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +163 -12
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +163 -12
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +32 -2
- package/dist/index.d.ts +32 -2
- package/dist/index.js +232 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +232 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/core/navigators/Pipeline.ts +5 -2
- package/src/core/navigators/PipelineDebugger.ts +238 -8
- package/src/study/SessionController.ts +88 -11
- package/src/study/SessionDebugger.ts +8 -0
package/dist/index.d.cts
CHANGED
|
@@ -478,8 +478,19 @@ declare class SessionController<TView = unknown> extends Loggable {
|
|
|
478
478
|
/**
|
|
479
479
|
* Request a mid-session replan. Re-runs the pipeline with current user state
|
|
480
480
|
* and atomically replaces the newQ contents. Safe to call at any time during
|
|
481
|
-
* a session
|
|
482
|
-
*
|
|
481
|
+
* a session.
|
|
482
|
+
*
|
|
483
|
+
* Concurrency policy:
|
|
484
|
+
* - Two unhinted auto-replans never run in parallel; the second coalesces
|
|
485
|
+
* into the first (returns the same promise).
|
|
486
|
+
* - A hint-bearing replan that arrives while another replan is in flight
|
|
487
|
+
* is queued to run **after** the in-flight one rather than dropped.
|
|
488
|
+
* This preserves caller intent (label, requireCards, excludeTags,
|
|
489
|
+
* limit, minFollowUpCards) instead of silently discarding it. Without
|
|
490
|
+
* queueing, a background auto-replan that started just before a
|
|
491
|
+
* completion-triggered replan would clobber the queue with unhinted
|
|
492
|
+
* results (e.g. surfacing another gpc-intro card right after one
|
|
493
|
+
* completed, skipping the prescribed `c-wst-*` follow-up).
|
|
483
494
|
*
|
|
484
495
|
* Does NOT affect reviewQ or failedQ.
|
|
485
496
|
*
|
|
@@ -491,6 +502,25 @@ declare class SessionController<TView = unknown> extends Loggable {
|
|
|
491
502
|
* calls this to ensure newly-unlocked content appears in the session.
|
|
492
503
|
*/
|
|
493
504
|
requestReplan(options?: ReplanOptions | ReplanHints): Promise<void>;
|
|
505
|
+
/**
|
|
506
|
+
* True when a requestReplan call carries caller intent that must not be
|
|
507
|
+
* silently dropped. Bare unhinted auto-replans (depletion / quality
|
|
508
|
+
* triggers in nextCard) return false and may coalesce.
|
|
509
|
+
*/
|
|
510
|
+
private _replanHasIntent;
|
|
511
|
+
/**
|
|
512
|
+
* Body of a single replan: populate auto-excludes, stash hints on
|
|
513
|
+
* sources, log, then run the pipeline. Extracted so it can be invoked
|
|
514
|
+
* either immediately (no in-flight replan) or queued (chained after
|
|
515
|
+
* the in-flight one resolves).
|
|
516
|
+
*
|
|
517
|
+
* IMPORTANT: hint stash and the queue-state snapshot used to build
|
|
518
|
+
* excludeCards happen at *invocation* time, not at *queue* time. For a
|
|
519
|
+
* queued replan that means excludes reflect the state after the prior
|
|
520
|
+
* replan landed — which is what we want, since the prior replan's
|
|
521
|
+
* newQ.peek(0) is the imminent draw we need to exclude.
|
|
522
|
+
*/
|
|
523
|
+
private _runReplan;
|
|
494
524
|
/**
|
|
495
525
|
* Normalise the requestReplan argument. Accepts either a ReplanOptions
|
|
496
526
|
* object (new API) or a plain Record<string, unknown> (legacy callers
|
package/dist/index.d.ts
CHANGED
|
@@ -478,8 +478,19 @@ declare class SessionController<TView = unknown> extends Loggable {
|
|
|
478
478
|
/**
|
|
479
479
|
* Request a mid-session replan. Re-runs the pipeline with current user state
|
|
480
480
|
* and atomically replaces the newQ contents. Safe to call at any time during
|
|
481
|
-
* a session
|
|
482
|
-
*
|
|
481
|
+
* a session.
|
|
482
|
+
*
|
|
483
|
+
* Concurrency policy:
|
|
484
|
+
* - Two unhinted auto-replans never run in parallel; the second coalesces
|
|
485
|
+
* into the first (returns the same promise).
|
|
486
|
+
* - A hint-bearing replan that arrives while another replan is in flight
|
|
487
|
+
* is queued to run **after** the in-flight one rather than dropped.
|
|
488
|
+
* This preserves caller intent (label, requireCards, excludeTags,
|
|
489
|
+
* limit, minFollowUpCards) instead of silently discarding it. Without
|
|
490
|
+
* queueing, a background auto-replan that started just before a
|
|
491
|
+
* completion-triggered replan would clobber the queue with unhinted
|
|
492
|
+
* results (e.g. surfacing another gpc-intro card right after one
|
|
493
|
+
* completed, skipping the prescribed `c-wst-*` follow-up).
|
|
483
494
|
*
|
|
484
495
|
* Does NOT affect reviewQ or failedQ.
|
|
485
496
|
*
|
|
@@ -491,6 +502,25 @@ declare class SessionController<TView = unknown> extends Loggable {
|
|
|
491
502
|
* calls this to ensure newly-unlocked content appears in the session.
|
|
492
503
|
*/
|
|
493
504
|
requestReplan(options?: ReplanOptions | ReplanHints): Promise<void>;
|
|
505
|
+
/**
|
|
506
|
+
* True when a requestReplan call carries caller intent that must not be
|
|
507
|
+
* silently dropped. Bare unhinted auto-replans (depletion / quality
|
|
508
|
+
* triggers in nextCard) return false and may coalesce.
|
|
509
|
+
*/
|
|
510
|
+
private _replanHasIntent;
|
|
511
|
+
/**
|
|
512
|
+
* Body of a single replan: populate auto-excludes, stash hints on
|
|
513
|
+
* sources, log, then run the pipeline. Extracted so it can be invoked
|
|
514
|
+
* either immediately (no in-flight replan) or queued (chained after
|
|
515
|
+
* the in-flight one resolves).
|
|
516
|
+
*
|
|
517
|
+
* IMPORTANT: hint stash and the queue-state snapshot used to build
|
|
518
|
+
* excludeCards happen at *invocation* time, not at *queue* time. For a
|
|
519
|
+
* queued replan that means excludes reflect the state after the prior
|
|
520
|
+
* replan landed — which is what we want, since the prior replan's
|
|
521
|
+
* newQ.peek(0) is the imminent draw we need to exclude.
|
|
522
|
+
*/
|
|
523
|
+
private _runReplan;
|
|
494
524
|
/**
|
|
495
525
|
* Normalise the requestReplan argument. Accepts either a ReplanOptions
|
|
496
526
|
* object (new API) or a plain Record<string, unknown> (legacy callers
|
package/dist/index.js
CHANGED
|
@@ -877,6 +877,7 @@ var PipelineDebugger_exports = {};
|
|
|
877
877
|
__export(PipelineDebugger_exports, {
|
|
878
878
|
buildRunReport: () => buildRunReport,
|
|
879
879
|
captureRun: () => captureRun,
|
|
880
|
+
clearRunHistory: () => clearRunHistory,
|
|
880
881
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
881
882
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
882
883
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -884,6 +885,9 @@ __export(PipelineDebugger_exports, {
|
|
|
884
885
|
function registerPipelineForDebug(pipeline) {
|
|
885
886
|
_activePipeline = pipeline;
|
|
886
887
|
}
|
|
888
|
+
function clearRunHistory() {
|
|
889
|
+
runHistory.length = 0;
|
|
890
|
+
}
|
|
887
891
|
function getOrigin(card) {
|
|
888
892
|
const firstEntry = card.provenance[0];
|
|
889
893
|
if (!firstEntry) return "unknown";
|
|
@@ -912,7 +916,7 @@ function parseCardElo(provenance) {
|
|
|
912
916
|
}
|
|
913
917
|
function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
|
|
914
918
|
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
915
|
-
const
|
|
919
|
+
const toReport = (card) => ({
|
|
916
920
|
cardId: card.cardId,
|
|
917
921
|
courseId: card.courseId,
|
|
918
922
|
origin: getOrigin(card),
|
|
@@ -922,7 +926,47 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
922
926
|
provenance: card.provenance,
|
|
923
927
|
tags: card.tags,
|
|
924
928
|
selected: selectedIds.has(card.cardId)
|
|
925
|
-
})
|
|
929
|
+
});
|
|
930
|
+
const selectedReported = [];
|
|
931
|
+
const nearMissReported = [];
|
|
932
|
+
const discardedTailCards = [];
|
|
933
|
+
let nonSelectedSeen = 0;
|
|
934
|
+
for (const card of allCards) {
|
|
935
|
+
if (selectedIds.has(card.cardId)) {
|
|
936
|
+
selectedReported.push(toReport(card));
|
|
937
|
+
} else if (nonSelectedSeen < DISCARDED_KEEP_TOP) {
|
|
938
|
+
nearMissReported.push(toReport(card));
|
|
939
|
+
nonSelectedSeen++;
|
|
940
|
+
} else {
|
|
941
|
+
discardedTailCards.push(card);
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
const cards = [...selectedReported, ...nearMissReported];
|
|
945
|
+
let discardedTail;
|
|
946
|
+
if (discardedTailCards.length > 0) {
|
|
947
|
+
let scoreMin = Infinity;
|
|
948
|
+
let scoreMax = -Infinity;
|
|
949
|
+
let eloMin = Infinity;
|
|
950
|
+
let eloMax = -Infinity;
|
|
951
|
+
let eloSeen = false;
|
|
952
|
+
for (const c of discardedTailCards) {
|
|
953
|
+
if (c.score < scoreMin) scoreMin = c.score;
|
|
954
|
+
if (c.score > scoreMax) scoreMax = c.score;
|
|
955
|
+
const elo = parseCardElo(c.provenance);
|
|
956
|
+
if (elo !== void 0) {
|
|
957
|
+
eloSeen = true;
|
|
958
|
+
if (elo < eloMin) eloMin = elo;
|
|
959
|
+
if (elo > eloMax) eloMax = elo;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
const eloFragment = eloSeen ? `, ELO ${eloMin}\u2013${eloMax}` : "";
|
|
963
|
+
discardedTail = {
|
|
964
|
+
count: discardedTailCards.length,
|
|
965
|
+
scoreRange: [scoreMin, scoreMax],
|
|
966
|
+
eloRange: eloSeen ? [eloMin, eloMax] : void 0,
|
|
967
|
+
note: `${discardedTailCards.length} additional candidate(s) scored below the top ${DISCARDED_KEEP_TOP} near-misses and were not retained (score ${scoreMin.toExponential(2)}\u2013${scoreMax.toExponential(2)}${eloFragment}). Likely ELO-window pull remnants filtered out by hierarchy/lesson/priority gates.`
|
|
968
|
+
};
|
|
969
|
+
}
|
|
926
970
|
const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
|
|
927
971
|
const newSelected = selectedCards.filter((c) => getOrigin(c) === "new").length;
|
|
928
972
|
return {
|
|
@@ -937,7 +981,8 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
937
981
|
finalCount: selectedCards.length,
|
|
938
982
|
reviewsSelected,
|
|
939
983
|
newSelected,
|
|
940
|
-
cards
|
|
984
|
+
cards,
|
|
985
|
+
discardedTail
|
|
941
986
|
};
|
|
942
987
|
}
|
|
943
988
|
function formatProvenance(provenance) {
|
|
@@ -973,6 +1018,44 @@ function printRunSummary(run) {
|
|
|
973
1018
|
);
|
|
974
1019
|
console.groupEnd();
|
|
975
1020
|
}
|
|
1021
|
+
function escapeHtml(s) {
|
|
1022
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1023
|
+
}
|
|
1024
|
+
function escapeAttr(s) {
|
|
1025
|
+
return escapeHtml(s).replace(/"/g, """);
|
|
1026
|
+
}
|
|
1027
|
+
function copyTextToClipboard(text, btn) {
|
|
1028
|
+
const done = () => {
|
|
1029
|
+
if (!btn) return;
|
|
1030
|
+
const orig = btn.textContent ?? "Copy";
|
|
1031
|
+
btn.textContent = "Copied!";
|
|
1032
|
+
btn.classList.add("copied");
|
|
1033
|
+
setTimeout(() => {
|
|
1034
|
+
btn.textContent = orig;
|
|
1035
|
+
btn.classList.remove("copied");
|
|
1036
|
+
}, 1200);
|
|
1037
|
+
};
|
|
1038
|
+
const fallback = () => {
|
|
1039
|
+
const ta = document.createElement("textarea");
|
|
1040
|
+
ta.value = text;
|
|
1041
|
+
ta.style.position = "fixed";
|
|
1042
|
+
ta.style.opacity = "0";
|
|
1043
|
+
document.body.appendChild(ta);
|
|
1044
|
+
ta.select();
|
|
1045
|
+
try {
|
|
1046
|
+
document.execCommand("copy");
|
|
1047
|
+
} catch (e) {
|
|
1048
|
+
logger.warn(`[Pipeline Debug] Copy failed: ${e}`);
|
|
1049
|
+
}
|
|
1050
|
+
document.body.removeChild(ta);
|
|
1051
|
+
done();
|
|
1052
|
+
};
|
|
1053
|
+
if (navigator.clipboard?.writeText) {
|
|
1054
|
+
navigator.clipboard.writeText(text).then(done).catch(fallback);
|
|
1055
|
+
} else {
|
|
1056
|
+
fallback();
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
976
1059
|
function renderUI() {
|
|
977
1060
|
if (!_uiContainer) return;
|
|
978
1061
|
const runs = runHistory;
|
|
@@ -1031,6 +1114,13 @@ function renderUI() {
|
|
|
1031
1114
|
#sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
|
|
1032
1115
|
#sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
|
|
1033
1116
|
#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; }
|
|
1117
|
+
#sk-pipeline-debugger .run-label { display: inline-block; margin-top: 0.25rem; padding: 0.1rem 0.4rem; background: #fff3cd; color: #664d03; border-radius: 3px; font-family: monospace; font-size: 11px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
|
|
1118
|
+
#sk-pipeline-debugger .label-banner { display: inline-block; padding: 0.25rem 0.6rem; background: #fff3cd; color: #664d03; border-radius: 4px; font-family: monospace; font-size: 13px; margin: 0 0 0.75rem 0; }
|
|
1119
|
+
#sk-pipeline-debugger .copy-btn { background: #0d6efd; color: white; border: none; padding: 0.25rem 0.6rem; border-radius: 3px; cursor: pointer; font-size: 12px; margin-left: 0.5rem; }
|
|
1120
|
+
#sk-pipeline-debugger .copy-btn:hover { background: #0b5ed7; }
|
|
1121
|
+
#sk-pipeline-debugger .copy-btn.copied { background: #198754; }
|
|
1122
|
+
#sk-pipeline-debugger .section-head { display: flex; align-items: center; justify-content: space-between; margin-top: 1rem; }
|
|
1123
|
+
#sk-pipeline-debugger .section-head h3 { margin: 0; }
|
|
1034
1124
|
`;
|
|
1035
1125
|
const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
|
|
1036
1126
|
(r, i) => `
|
|
@@ -1038,6 +1128,7 @@ function renderUI() {
|
|
|
1038
1128
|
<strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
|
|
1039
1129
|
<small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
|
|
1040
1130
|
<small>${r.finalCount} cards selected</small>
|
|
1131
|
+
${r.hints?._label ? `<br/><span class="run-label" title="${escapeAttr(r.hints._label)}">${escapeHtml(r.hints._label)}</span>` : ""}
|
|
1041
1132
|
</div>
|
|
1042
1133
|
`
|
|
1043
1134
|
).join("");
|
|
@@ -1046,11 +1137,13 @@ function renderUI() {
|
|
|
1046
1137
|
const filteredCards = selectedRun.cards.filter(
|
|
1047
1138
|
(c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
|
|
1048
1139
|
);
|
|
1140
|
+
const labelText = selectedRun.hints?._label ?? "(no label)";
|
|
1049
1141
|
detailsHtml = `
|
|
1050
1142
|
<h2>Run: ${selectedRun.runId}</h2>
|
|
1143
|
+
<div class="label-banner" title="${escapeAttr(labelText)}">${escapeHtml(labelText)}</div>
|
|
1051
1144
|
<p>
|
|
1052
|
-
<strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
|
|
1053
|
-
<strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
|
|
1145
|
+
<strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
|
|
1146
|
+
<strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
|
|
1054
1147
|
<strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
|
|
1055
1148
|
</p>
|
|
1056
1149
|
|
|
@@ -1065,7 +1158,10 @@ function renderUI() {
|
|
|
1065
1158
|
</table>
|
|
1066
1159
|
|
|
1067
1160
|
${selectedRun.hints ? `
|
|
1068
|
-
<
|
|
1161
|
+
<div class="section-head">
|
|
1162
|
+
<h3>Ephemeral Hints</h3>
|
|
1163
|
+
<button class="copy-btn" onclick="window.skuilder.pipeline._copyConfig('${selectedRun.runId}', this)">Copy config</button>
|
|
1164
|
+
</div>
|
|
1069
1165
|
<table>
|
|
1070
1166
|
${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
|
|
1071
1167
|
${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
|
|
@@ -1089,7 +1185,10 @@ function renderUI() {
|
|
|
1089
1185
|
</tbody>
|
|
1090
1186
|
</table>
|
|
1091
1187
|
|
|
1092
|
-
<
|
|
1188
|
+
<div class="section-head">
|
|
1189
|
+
<h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
|
|
1190
|
+
<button class="copy-btn" onclick="window.skuilder.pipeline._copyResults('${selectedRun.runId}', this)">Copy results</button>
|
|
1191
|
+
</div>
|
|
1093
1192
|
<input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
|
|
1094
1193
|
|
|
1095
1194
|
<table>
|
|
@@ -1135,7 +1234,7 @@ function mountPipelineDebugger() {
|
|
|
1135
1234
|
win.skuilder = win.skuilder || {};
|
|
1136
1235
|
win.skuilder.pipeline = pipelineDebugAPI;
|
|
1137
1236
|
}
|
|
1138
|
-
var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
|
|
1237
|
+
var _activePipeline, MAX_RUNS, runHistory, DISCARDED_KEEP_TOP, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
|
|
1139
1238
|
var init_PipelineDebugger = __esm({
|
|
1140
1239
|
"src/core/navigators/PipelineDebugger.ts"() {
|
|
1141
1240
|
"use strict";
|
|
@@ -1144,6 +1243,7 @@ var init_PipelineDebugger = __esm({
|
|
|
1144
1243
|
_activePipeline = null;
|
|
1145
1244
|
MAX_RUNS = 10;
|
|
1146
1245
|
runHistory = [];
|
|
1246
|
+
DISCARDED_KEEP_TOP = 25;
|
|
1147
1247
|
_uiContainer = null;
|
|
1148
1248
|
_selectedRunIndex = null;
|
|
1149
1249
|
_cardSearchQuery = "";
|
|
@@ -1208,7 +1308,14 @@ var init_PipelineDebugger = __esm({
|
|
|
1208
1308
|
return;
|
|
1209
1309
|
}
|
|
1210
1310
|
}
|
|
1211
|
-
|
|
1311
|
+
const runsWithTails = runHistory.filter((r) => r.discardedTail && r.discardedTail.count > 0);
|
|
1312
|
+
if (runsWithTails.length > 0) {
|
|
1313
|
+
logger.info(
|
|
1314
|
+
`[Pipeline Debug] Card '${cardId}' not found in retained cards. ${runsWithTails.length} run(s) have discarded tails that were not retained \u2014 the card may have been a low-score candidate. See run.discardedTail for ranges.`
|
|
1315
|
+
);
|
|
1316
|
+
} else {
|
|
1317
|
+
logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
|
|
1318
|
+
}
|
|
1212
1319
|
},
|
|
1213
1320
|
/**
|
|
1214
1321
|
* Explain why reviews may or may not have been selected.
|
|
@@ -1513,6 +1620,50 @@ var init_PipelineDebugger = __esm({
|
|
|
1513
1620
|
_cardSearchQuery = query;
|
|
1514
1621
|
renderUI();
|
|
1515
1622
|
},
|
|
1623
|
+
/**
|
|
1624
|
+
* Internal UI helpers
|
|
1625
|
+
* @internal
|
|
1626
|
+
*/
|
|
1627
|
+
_copyConfig(runId, btn) {
|
|
1628
|
+
const run = runHistory.find((r) => r.runId === runId);
|
|
1629
|
+
if (!run) return;
|
|
1630
|
+
const payload = {
|
|
1631
|
+
runId: run.runId,
|
|
1632
|
+
timestamp: run.timestamp.toISOString(),
|
|
1633
|
+
courseId: run.courseId,
|
|
1634
|
+
courseName: run.courseName,
|
|
1635
|
+
hints: run.hints ?? null
|
|
1636
|
+
};
|
|
1637
|
+
copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
|
|
1638
|
+
},
|
|
1639
|
+
/**
|
|
1640
|
+
* Internal UI helpers
|
|
1641
|
+
* @internal
|
|
1642
|
+
*
|
|
1643
|
+
* Copies an "abridged" view of results: just the selected cards with their
|
|
1644
|
+
* generator, origin, final score, and the top provenance reason. Designed
|
|
1645
|
+
* for pasting into bug reports without flooding with full provenance.
|
|
1646
|
+
*/
|
|
1647
|
+
_copyResults(runId, btn) {
|
|
1648
|
+
const run = runHistory.find((r) => r.runId === runId);
|
|
1649
|
+
if (!run) return;
|
|
1650
|
+
const selected = run.cards.filter((c) => c.selected).sort((a, b) => b.finalScore - a.finalScore).map((c) => ({
|
|
1651
|
+
cardId: c.cardId,
|
|
1652
|
+
generator: c.generator,
|
|
1653
|
+
origin: c.origin,
|
|
1654
|
+
score: Number(c.finalScore.toFixed(3)),
|
|
1655
|
+
topReason: c.provenance[0]?.reason ?? ""
|
|
1656
|
+
}));
|
|
1657
|
+
const payload = {
|
|
1658
|
+
runId: run.runId,
|
|
1659
|
+
label: run.hints?._label ?? null,
|
|
1660
|
+
finalCount: run.finalCount,
|
|
1661
|
+
newSelected: run.newSelected,
|
|
1662
|
+
reviewsSelected: run.reviewsSelected,
|
|
1663
|
+
selected
|
|
1664
|
+
};
|
|
1665
|
+
copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
|
|
1666
|
+
},
|
|
1516
1667
|
/**
|
|
1517
1668
|
* Show help.
|
|
1518
1669
|
*/
|
|
@@ -5822,12 +5973,12 @@ ${e.stack}` : JSON.stringify(e);
|
|
|
5822
5973
|
async getWeightedCards(limit) {
|
|
5823
5974
|
const u = await this._getCurrentUser();
|
|
5824
5975
|
try {
|
|
5825
|
-
const
|
|
5976
|
+
const navigator2 = await this.createNavigator(u);
|
|
5826
5977
|
if (this._pendingHints) {
|
|
5827
|
-
|
|
5978
|
+
navigator2.setEphemeralHints(this._pendingHints);
|
|
5828
5979
|
this._pendingHints = null;
|
|
5829
5980
|
}
|
|
5830
|
-
return
|
|
5981
|
+
return navigator2.getWeightedCards(limit);
|
|
5831
5982
|
} catch (e) {
|
|
5832
5983
|
logger.error(`[courseDB] Error getting weighted cards: ${e}`);
|
|
5833
5984
|
throw e;
|
|
@@ -8872,12 +9023,12 @@ var init_courseDB2 = __esm({
|
|
|
8872
9023
|
}
|
|
8873
9024
|
async getWeightedCards(limit) {
|
|
8874
9025
|
try {
|
|
8875
|
-
const
|
|
9026
|
+
const navigator2 = await this.createNavigator(this.userDB);
|
|
8876
9027
|
if (this._pendingHints) {
|
|
8877
|
-
|
|
9028
|
+
navigator2.setEphemeralHints(this._pendingHints);
|
|
8878
9029
|
this._pendingHints = null;
|
|
8879
9030
|
}
|
|
8880
|
-
return
|
|
9031
|
+
return navigator2.getWeightedCards(limit);
|
|
8881
9032
|
} catch (e) {
|
|
8882
9033
|
logger.error(`[static/courseDB] Error getting weighted cards: ${e}`);
|
|
8883
9034
|
throw e;
|
|
@@ -12912,10 +13063,12 @@ mountMixerDebugger();
|
|
|
12912
13063
|
|
|
12913
13064
|
// src/study/SessionDebugger.ts
|
|
12914
13065
|
init_logger();
|
|
13066
|
+
init_PipelineDebugger();
|
|
12915
13067
|
var activeSession = null;
|
|
12916
13068
|
var sessionHistory = [];
|
|
12917
13069
|
var MAX_HISTORY = 5;
|
|
12918
13070
|
function startSessionTracking(reviewQLength, newQLength, failedQLength) {
|
|
13071
|
+
clearRunHistory();
|
|
12919
13072
|
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
12920
13073
|
activeSession = {
|
|
12921
13074
|
sessionId,
|
|
@@ -13373,8 +13526,19 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13373
13526
|
/**
|
|
13374
13527
|
* Request a mid-session replan. Re-runs the pipeline with current user state
|
|
13375
13528
|
* and atomically replaces the newQ contents. Safe to call at any time during
|
|
13376
|
-
* a session
|
|
13377
|
-
*
|
|
13529
|
+
* a session.
|
|
13530
|
+
*
|
|
13531
|
+
* Concurrency policy:
|
|
13532
|
+
* - Two unhinted auto-replans never run in parallel; the second coalesces
|
|
13533
|
+
* into the first (returns the same promise).
|
|
13534
|
+
* - A hint-bearing replan that arrives while another replan is in flight
|
|
13535
|
+
* is queued to run **after** the in-flight one rather than dropped.
|
|
13536
|
+
* This preserves caller intent (label, requireCards, excludeTags,
|
|
13537
|
+
* limit, minFollowUpCards) instead of silently discarding it. Without
|
|
13538
|
+
* queueing, a background auto-replan that started just before a
|
|
13539
|
+
* completion-triggered replan would clobber the queue with unhinted
|
|
13540
|
+
* results (e.g. surfacing another gpc-intro card right after one
|
|
13541
|
+
* completed, skipping the prescribed `c-wst-*` follow-up).
|
|
13378
13542
|
*
|
|
13379
13543
|
* Does NOT affect reviewQ or failedQ.
|
|
13380
13544
|
*
|
|
@@ -13390,10 +13554,55 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13390
13554
|
if (opts.hints || opts.label || opts.limit) {
|
|
13391
13555
|
this._depletionReplanAttempted = false;
|
|
13392
13556
|
}
|
|
13557
|
+
const hasIntent = this._replanHasIntent(opts);
|
|
13393
13558
|
if (this._replanPromise) {
|
|
13394
|
-
|
|
13395
|
-
|
|
13559
|
+
if (!hasIntent) {
|
|
13560
|
+
this.log("Replan already in progress, coalescing unhinted auto-replan");
|
|
13561
|
+
return this._replanPromise;
|
|
13562
|
+
}
|
|
13563
|
+
const labelTag = opts.label ? ` [${opts.label}]` : "";
|
|
13564
|
+
this.log(
|
|
13565
|
+
`Replan in progress; queueing hint-bearing replan${labelTag} behind in-flight run`
|
|
13566
|
+
);
|
|
13567
|
+
const inflight = this._replanPromise;
|
|
13568
|
+
const queued = inflight.catch(() => void 0).then(() => this._runReplan(opts));
|
|
13569
|
+
this._replanPromise = queued.finally(() => {
|
|
13570
|
+
if (this._replanPromise === queued) this._replanPromise = null;
|
|
13571
|
+
});
|
|
13572
|
+
return queued;
|
|
13396
13573
|
}
|
|
13574
|
+
const run = this._runReplan(opts);
|
|
13575
|
+
this._replanPromise = run.finally(() => {
|
|
13576
|
+
if (this._replanPromise === run) this._replanPromise = null;
|
|
13577
|
+
});
|
|
13578
|
+
await run;
|
|
13579
|
+
}
|
|
13580
|
+
/**
|
|
13581
|
+
* True when a requestReplan call carries caller intent that must not be
|
|
13582
|
+
* silently dropped. Bare unhinted auto-replans (depletion / quality
|
|
13583
|
+
* triggers in nextCard) return false and may coalesce.
|
|
13584
|
+
*/
|
|
13585
|
+
_replanHasIntent(opts) {
|
|
13586
|
+
if (opts.label) return true;
|
|
13587
|
+
if (opts.limit !== void 0) return true;
|
|
13588
|
+
if (opts.minFollowUpCards !== void 0) return true;
|
|
13589
|
+
if (opts.mode && opts.mode !== "replace") return true;
|
|
13590
|
+
if (opts.hints && Object.keys(opts.hints).length > 0) return true;
|
|
13591
|
+
return false;
|
|
13592
|
+
}
|
|
13593
|
+
/**
|
|
13594
|
+
* Body of a single replan: populate auto-excludes, stash hints on
|
|
13595
|
+
* sources, log, then run the pipeline. Extracted so it can be invoked
|
|
13596
|
+
* either immediately (no in-flight replan) or queued (chained after
|
|
13597
|
+
* the in-flight one resolves).
|
|
13598
|
+
*
|
|
13599
|
+
* IMPORTANT: hint stash and the queue-state snapshot used to build
|
|
13600
|
+
* excludeCards happen at *invocation* time, not at *queue* time. For a
|
|
13601
|
+
* queued replan that means excludes reflect the state after the prior
|
|
13602
|
+
* replan landed — which is what we want, since the prior replan's
|
|
13603
|
+
* newQ.peek(0) is the imminent draw we need to exclude.
|
|
13604
|
+
*/
|
|
13605
|
+
async _runReplan(opts) {
|
|
13397
13606
|
if (!opts.hints) opts.hints = {};
|
|
13398
13607
|
const hints = opts.hints;
|
|
13399
13608
|
const excludeSet = new Set(hints.excludeCards ?? []);
|
|
@@ -13403,6 +13612,9 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13403
13612
|
for (const rec of this._sessionRecord) {
|
|
13404
13613
|
excludeSet.add(rec.card.card_id);
|
|
13405
13614
|
}
|
|
13615
|
+
if (this.newQ.length > 0) {
|
|
13616
|
+
excludeSet.add(this.newQ.peek(0).cardID);
|
|
13617
|
+
}
|
|
13406
13618
|
hints.excludeCards = [...excludeSet];
|
|
13407
13619
|
if (opts.hints) {
|
|
13408
13620
|
const hintsWithLabel = opts.label ? { ...opts.hints, _label: opts.label } : opts.hints;
|
|
@@ -13418,12 +13630,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13418
13630
|
this._minCardsGuarantee = Math.max(this._minCardsGuarantee, opts.minFollowUpCards);
|
|
13419
13631
|
this.log(`[Replan] Card guarantee set to ${this._minCardsGuarantee}`);
|
|
13420
13632
|
}
|
|
13421
|
-
|
|
13422
|
-
try {
|
|
13423
|
-
await this._replanPromise;
|
|
13424
|
-
} finally {
|
|
13425
|
-
this._replanPromise = null;
|
|
13426
|
-
}
|
|
13633
|
+
await this._executeReplan(opts);
|
|
13427
13634
|
}
|
|
13428
13635
|
/**
|
|
13429
13636
|
* Normalise the requestReplan argument. Accepts either a ReplanOptions
|