@vue-skuilder/db 0.2.4 → 0.2.5
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.js +5 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +5 -4
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +5 -4
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +5 -4
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +5 -4
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +5 -4
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +58 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.js +207 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +207 -19
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/core/navigators/generators/elo.ts +32 -11
- package/src/study/SessionController.ts +104 -3
- package/src/study/SessionOverlay.ts +245 -21
package/dist/index.mjs
CHANGED
|
@@ -1917,12 +1917,13 @@ __export(elo_exports, {
|
|
|
1917
1917
|
default: () => ELONavigator
|
|
1918
1918
|
});
|
|
1919
1919
|
import { toCourseElo as toCourseElo2 } from "@vue-skuilder/common";
|
|
1920
|
-
var ELONavigator;
|
|
1920
|
+
var ELO_RELEVANCE_SIGMA, ELONavigator;
|
|
1921
1921
|
var init_elo = __esm({
|
|
1922
1922
|
"src/core/navigators/generators/elo.ts"() {
|
|
1923
1923
|
"use strict";
|
|
1924
1924
|
init_navigators();
|
|
1925
1925
|
init_logger();
|
|
1926
|
+
ELO_RELEVANCE_SIGMA = 300;
|
|
1926
1927
|
ELONavigator = class extends ContentNavigator {
|
|
1927
1928
|
/** Human-readable name for CardGenerator interface */
|
|
1928
1929
|
name;
|
|
@@ -1962,8 +1963,8 @@ var init_elo = __esm({
|
|
|
1962
1963
|
const scored = newCards.map((c) => {
|
|
1963
1964
|
const cardElo = c.elo ?? 1e3;
|
|
1964
1965
|
const distance = Math.abs(cardElo - userGlobalElo);
|
|
1965
|
-
const
|
|
1966
|
-
const samplingKey =
|
|
1966
|
+
const relevance = Math.exp(-((distance / ELO_RELEVANCE_SIGMA) ** 2));
|
|
1967
|
+
const samplingKey = relevance * (0.5 + 0.5 * Math.random());
|
|
1967
1968
|
return {
|
|
1968
1969
|
cardId: c.cardID,
|
|
1969
1970
|
courseId: c.courseID,
|
|
@@ -1975,7 +1976,7 @@ var init_elo = __esm({
|
|
|
1975
1976
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-ELO-default",
|
|
1976
1977
|
action: "generated",
|
|
1977
1978
|
score: samplingKey,
|
|
1978
|
-
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}),
|
|
1979
|
+
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), relevance ${relevance.toFixed(3)}, key ${samplingKey.toFixed(3)}`
|
|
1979
1980
|
}
|
|
1980
1981
|
]
|
|
1981
1982
|
};
|
|
@@ -13071,7 +13072,15 @@ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834"
|
|
|
13071
13072
|
var spinnerFrame = 0;
|
|
13072
13073
|
var overlayEl = null;
|
|
13073
13074
|
var pollHandle = null;
|
|
13074
|
-
var
|
|
13075
|
+
var lastSnapshot = null;
|
|
13076
|
+
var copyFlashUntil = 0;
|
|
13077
|
+
var minified = false;
|
|
13078
|
+
var expanded = {
|
|
13079
|
+
reviewQ: false,
|
|
13080
|
+
newQ: false,
|
|
13081
|
+
failedQ: false,
|
|
13082
|
+
drawn: false
|
|
13083
|
+
};
|
|
13075
13084
|
function toggleSessionOverlay() {
|
|
13076
13085
|
if (typeof document === "undefined") {
|
|
13077
13086
|
logger.info("[Session Overlay] No DOM available (non-browser host); overlay unavailable.");
|
|
@@ -13086,6 +13095,7 @@ function toggleSessionOverlay() {
|
|
|
13086
13095
|
}
|
|
13087
13096
|
}
|
|
13088
13097
|
function mount() {
|
|
13098
|
+
minified = false;
|
|
13089
13099
|
overlayEl = document.createElement("div");
|
|
13090
13100
|
overlayEl.id = OVERLAY_ID;
|
|
13091
13101
|
Object.assign(overlayEl.style, {
|
|
@@ -13124,11 +13134,23 @@ function render() {
|
|
|
13124
13134
|
spinnerFrame++;
|
|
13125
13135
|
const ctrl = getActiveController();
|
|
13126
13136
|
if (!ctrl) {
|
|
13127
|
-
|
|
13137
|
+
lastSnapshot = null;
|
|
13138
|
+
overlayEl.innerHTML = headerHtml() + (minified ? "" : `<div style="opacity:.65">No active session.</div>`);
|
|
13139
|
+
attachHandlers();
|
|
13128
13140
|
return;
|
|
13129
13141
|
}
|
|
13130
13142
|
const s = ctrl.getDebugSnapshot();
|
|
13131
|
-
|
|
13143
|
+
lastSnapshot = s;
|
|
13144
|
+
if (minified) {
|
|
13145
|
+
overlayEl.innerHTML = headerHtml();
|
|
13146
|
+
attachHandlers();
|
|
13147
|
+
return;
|
|
13148
|
+
}
|
|
13149
|
+
overlayEl.innerHTML = headerHtml() + replanHtml(s) + metaHtml(s) + hintsHtml(s.sessionHints) + queueHtml("reviewQ", "reviewQ", s.reviewQ) + queueHtml("newQ", "newQ", s.newQ) + queueHtml("failedQ", "failedQ", s.failedQ) + drawnHtml("drawn", s.drawnCards);
|
|
13150
|
+
attachHandlers();
|
|
13151
|
+
}
|
|
13152
|
+
function attachHandlers() {
|
|
13153
|
+
if (!overlayEl) return;
|
|
13132
13154
|
overlayEl.querySelectorAll("[data-q]").forEach((el) => {
|
|
13133
13155
|
el.onclick = () => {
|
|
13134
13156
|
const key = el.dataset.q;
|
|
@@ -13137,9 +13159,45 @@ function render() {
|
|
|
13137
13159
|
render();
|
|
13138
13160
|
};
|
|
13139
13161
|
});
|
|
13162
|
+
const copyBtn = overlayEl.querySelector("[data-copy]");
|
|
13163
|
+
if (copyBtn) {
|
|
13164
|
+
copyBtn.onclick = (ev) => {
|
|
13165
|
+
ev.stopPropagation();
|
|
13166
|
+
copySnapshot();
|
|
13167
|
+
};
|
|
13168
|
+
}
|
|
13169
|
+
const minBtn = overlayEl.querySelector("[data-min]");
|
|
13170
|
+
if (minBtn) {
|
|
13171
|
+
minBtn.onclick = (ev) => {
|
|
13172
|
+
ev.stopPropagation();
|
|
13173
|
+
minified = !minified;
|
|
13174
|
+
render();
|
|
13175
|
+
};
|
|
13176
|
+
}
|
|
13177
|
+
}
|
|
13178
|
+
function copySnapshot() {
|
|
13179
|
+
const text = snapshotToText(lastSnapshot);
|
|
13180
|
+
const flash = () => {
|
|
13181
|
+
copyFlashUntil = Date.now() + 1200;
|
|
13182
|
+
render();
|
|
13183
|
+
};
|
|
13184
|
+
const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
13185
|
+
if (clip?.writeText) {
|
|
13186
|
+
clip.writeText(text).then(flash, (err) => {
|
|
13187
|
+
logger.warn(`[Session Overlay] Clipboard write failed: ${String(err)}`);
|
|
13188
|
+
});
|
|
13189
|
+
} else {
|
|
13190
|
+
logger.info(`[Session Overlay] Clipboard unavailable; snapshot follows:
|
|
13191
|
+
${text}`);
|
|
13192
|
+
}
|
|
13140
13193
|
}
|
|
13141
13194
|
function headerHtml() {
|
|
13142
|
-
|
|
13195
|
+
const flashing = Date.now() < copyFlashUntil;
|
|
13196
|
+
const btnLabel = flashing ? "\u2713 copied" : "\u2398 copy";
|
|
13197
|
+
const btnColor = flashing ? "#86efac" : "#93c5fd";
|
|
13198
|
+
const copyBtn = `<span data-copy style="cursor:pointer;float:right;font-weight:400;color:${btnColor};border:1px solid currentColor;border-radius:4px;padding:0 4px;line-height:1.3">${btnLabel}</span>`;
|
|
13199
|
+
const minBtn = `<span data-min title="${minified ? "Restore" : "Minify"}" style="cursor:pointer;float:right;font-weight:400;color:#93c5fd;border:1px solid currentColor;border-radius:4px;padding:0 5px;margin-right:4px;line-height:1.3">${minified ? "\u25A2" : "\u2014"}</span>`;
|
|
13200
|
+
return `<div style="font-weight:600;color:#93c5fd;margin-bottom:${minified ? 0 : 4}px">${copyBtn}${minBtn}\u2699 SessionController</div>`;
|
|
13143
13201
|
}
|
|
13144
13202
|
function replanHtml(s) {
|
|
13145
13203
|
if (!s.replanActive) {
|
|
@@ -13182,22 +13240,98 @@ function hintsHtml(h) {
|
|
|
13182
13240
|
}
|
|
13183
13241
|
function queueHtml(key, label, q) {
|
|
13184
13242
|
const collapsible = q.length > INLINE_THRESHOLD;
|
|
13185
|
-
const isOpen =
|
|
13243
|
+
const isOpen = collapsible && expanded[key];
|
|
13186
13244
|
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13187
13245
|
const drawn = q.dequeueCount ? ` <span style="opacity:.5">drawn ${q.dequeueCount}</span>` : "";
|
|
13188
13246
|
const titleStyle = collapsible ? "cursor:pointer;color:#f9a8d4" : "color:#f9a8d4";
|
|
13189
13247
|
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13190
13248
|
const title = `<div${titleAttr} style="${titleStyle}">${caret}${label}: ${q.length}${drawn}</div>`;
|
|
13191
|
-
|
|
13192
|
-
|
|
13193
|
-
|
|
13194
|
-
|
|
13195
|
-
|
|
13196
|
-
|
|
13197
|
-
|
|
13249
|
+
if (!q.cards.length) {
|
|
13250
|
+
return title + `<div style="margin:1px 0 6px 6px;opacity:.5">empty</div>`;
|
|
13251
|
+
}
|
|
13252
|
+
const shown = isOpen ? q.cards : q.cards.slice(0, INLINE_THRESHOLD);
|
|
13253
|
+
const hiddenCount = q.length - shown.length;
|
|
13254
|
+
const listMarginBottom = collapsible ? 2 : 6;
|
|
13255
|
+
let body = `<ol style="margin:2px 0 ${listMarginBottom}px 0;padding-left:20px">` + shown.map((c) => `<li style="white-space:nowrap">${esc(c)}</li>`).join("") + `</ol>`;
|
|
13256
|
+
if (collapsible) {
|
|
13257
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13258
|
+
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
13259
|
+
}
|
|
13260
|
+
return title + body;
|
|
13261
|
+
}
|
|
13262
|
+
function outcomeGlyph(correct) {
|
|
13263
|
+
if (correct === true) return `<span style="color:#86efac">\u2713</span>`;
|
|
13264
|
+
if (correct === false) return `<span style="color:#fca5a5">\u2717</span>`;
|
|
13265
|
+
return `<span style="opacity:.5">\xB7</span>`;
|
|
13266
|
+
}
|
|
13267
|
+
function drawnHtml(key, drawn) {
|
|
13268
|
+
const collapsible = drawn.length > INLINE_THRESHOLD;
|
|
13269
|
+
const isOpen = collapsible && expanded[key];
|
|
13270
|
+
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13271
|
+
const titleStyle = collapsible ? "cursor:pointer;color:#c4b5fd" : "color:#c4b5fd";
|
|
13272
|
+
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13273
|
+
const title = `<div${titleAttr} style="${titleStyle}">${caret}drawn: ${drawn.length}</div>`;
|
|
13274
|
+
if (!drawn.length) {
|
|
13275
|
+
return title + `<div style="margin:1px 0 6px 6px;opacity:.5">none yet</div>`;
|
|
13276
|
+
}
|
|
13277
|
+
const shown = isOpen ? drawn : drawn.slice(0, INLINE_THRESHOLD);
|
|
13278
|
+
const hiddenCount = drawn.length - shown.length;
|
|
13279
|
+
const listMarginBottom = collapsible ? 2 : 6;
|
|
13280
|
+
const rows = shown.map((d) => {
|
|
13281
|
+
const retries = d.attempts > 1 ? `<span style="opacity:.5"> \xD7${d.attempts}</span>` : "";
|
|
13282
|
+
const time = `<span style="opacity:.45"> ${Math.round(d.timeSpentMs / 100) / 10}s</span>`;
|
|
13283
|
+
return `<li style="white-space:nowrap">${outcomeGlyph(d.correct)} ${esc(d.cardID)}<span style="opacity:.5"> [${esc(d.status)}]</span>${retries}${time}</li>`;
|
|
13284
|
+
}).join("");
|
|
13285
|
+
let body = `<ol style="margin:2px 0 ${listMarginBottom}px 0;padding-left:20px">${rows}</ol>`;
|
|
13286
|
+
if (collapsible) {
|
|
13287
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13288
|
+
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
13198
13289
|
}
|
|
13199
13290
|
return title + body;
|
|
13200
13291
|
}
|
|
13292
|
+
function snapshotToText(s) {
|
|
13293
|
+
if (!s) return "SessionController \u2014 no active session.";
|
|
13294
|
+
const lines = [];
|
|
13295
|
+
lines.push("=== SessionController ===");
|
|
13296
|
+
lines.push(`time ${formatTime(s.secondsRemaining)}`);
|
|
13297
|
+
if (s.hasCardGuarantee) lines.push(`guarantee: ${s.minCardsGuarantee}`);
|
|
13298
|
+
lines.push(`well-indicated left: ${s.wellIndicatedRemaining}`);
|
|
13299
|
+
lines.push(`current: ${s.currentCard ?? "\u2014"}`);
|
|
13300
|
+
lines.push(
|
|
13301
|
+
s.replanActive ? `replan: ACTIVE [${s.replanLabel ?? "(auto)"}]` : "replan: idle"
|
|
13302
|
+
);
|
|
13303
|
+
lines.push("");
|
|
13304
|
+
lines.push("sessionHints:");
|
|
13305
|
+
const h = s.sessionHints;
|
|
13306
|
+
const hintParts = [];
|
|
13307
|
+
if (h) {
|
|
13308
|
+
if (h.boostTags && Object.keys(h.boostTags).length)
|
|
13309
|
+
hintParts.push(` boost: ${Object.entries(h.boostTags).map(([k, v]) => `${k}\xD7${v}`).join(", ")}`);
|
|
13310
|
+
if (h.boostCards && Object.keys(h.boostCards).length)
|
|
13311
|
+
hintParts.push(` boostCards: ${Object.entries(h.boostCards).map(([k, v]) => `${k}\xD7${v}`).join(", ")}`);
|
|
13312
|
+
if (h.requireCards?.length) hintParts.push(` require: ${h.requireCards.join(", ")}`);
|
|
13313
|
+
if (h.requireTags?.length) hintParts.push(` requireTags: ${h.requireTags.join(", ")}`);
|
|
13314
|
+
if (h.excludeTags?.length) hintParts.push(` exclude: ${h.excludeTags.join(", ")}`);
|
|
13315
|
+
if (h.excludeCards?.length) hintParts.push(` excludeCards: ${h.excludeCards.join(", ")}`);
|
|
13316
|
+
}
|
|
13317
|
+
lines.push(hintParts.length ? hintParts.join("\n") : " none");
|
|
13318
|
+
const queueText = (label, q) => {
|
|
13319
|
+
lines.push("");
|
|
13320
|
+
lines.push(`${label}: ${q.length} (drawn ${q.dequeueCount})`);
|
|
13321
|
+
q.cards.forEach((c, i) => lines.push(` ${i + 1}. ${c}`));
|
|
13322
|
+
};
|
|
13323
|
+
queueText("reviewQ", s.reviewQ);
|
|
13324
|
+
queueText("newQ", s.newQ);
|
|
13325
|
+
queueText("failedQ", s.failedQ);
|
|
13326
|
+
lines.push("");
|
|
13327
|
+
lines.push(`drawn: ${s.drawnCards.length}`);
|
|
13328
|
+
s.drawnCards.forEach((d, i) => {
|
|
13329
|
+
const mark = d.correct === true ? "\u2713" : d.correct === false ? "\u2717" : "\xB7";
|
|
13330
|
+
const time = `${Math.round(d.timeSpentMs / 100) / 10}s`;
|
|
13331
|
+
lines.push(` ${i + 1}. ${mark} ${d.cardID} [${d.status}] \xD7${d.attempts} ${time}`);
|
|
13332
|
+
});
|
|
13333
|
+
return lines.join("\n");
|
|
13334
|
+
}
|
|
13201
13335
|
function formatTime(totalSeconds) {
|
|
13202
13336
|
const s = Math.max(0, Math.round(totalSeconds));
|
|
13203
13337
|
const m = Math.floor(s / 60);
|
|
@@ -13567,6 +13701,21 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13567
13701
|
* recomputed per-run in `_runReplan` and would otherwise go stale.
|
|
13568
13702
|
*/
|
|
13569
13703
|
_sessionHints = null;
|
|
13704
|
+
/**
|
|
13705
|
+
* Card IDs that have been *served* (drawn/consumed) this session. Populated
|
|
13706
|
+
* at the single consumption choke-point (removeItemFromQueue), so it reflects
|
|
13707
|
+
* a draw the instant it happens — earlier than `_sessionRecord`, which only
|
|
13708
|
+
* lands once the card is *responded to*.
|
|
13709
|
+
*
|
|
13710
|
+
* Used to keep already-served cards out of newQ on every (re)plan: a `new`
|
|
13711
|
+
* card shown once must never re-enter newQ this session. This is the general
|
|
13712
|
+
* guard against re-presentation — including the case where a replan in flight
|
|
13713
|
+
* captured a now-drawn card (e.g. a +INF require-injected follow-up the
|
|
13714
|
+
* depletion prefetch grabbed just before it was drawn). Reviews/failed cards
|
|
13715
|
+
* legitimately recur and are tracked by their own queues, so this only gates
|
|
13716
|
+
* `new`-origin candidates.
|
|
13717
|
+
*/
|
|
13718
|
+
_servedCardIds = /* @__PURE__ */ new Set();
|
|
13570
13719
|
/**
|
|
13571
13720
|
* Consumer-supplied hooks invoked after each question response is processed.
|
|
13572
13721
|
* Seeded from constructor options (threaded from
|
|
@@ -13776,6 +13925,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13776
13925
|
if (opts.mode && opts.mode !== "replace") return true;
|
|
13777
13926
|
if (opts.hints && Object.keys(opts.hints).length > 0) return true;
|
|
13778
13927
|
if (opts.sessionHints !== void 0) return true;
|
|
13928
|
+
if (opts.mergeSessionHints !== void 0) return true;
|
|
13779
13929
|
return false;
|
|
13780
13930
|
}
|
|
13781
13931
|
/**
|
|
@@ -13811,6 +13961,10 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13811
13961
|
`[Replan] Session hints ${opts.sessionHints ? "set" : "cleared"}: ${JSON.stringify(opts.sessionHints)}`
|
|
13812
13962
|
);
|
|
13813
13963
|
}
|
|
13964
|
+
if (opts.mergeSessionHints !== void 0) {
|
|
13965
|
+
this._sessionHints = mergeHints2([this._sessionHints, opts.mergeSessionHints]) ?? null;
|
|
13966
|
+
this.log(`[Replan] Session hints merged: ${JSON.stringify(this._sessionHints)}`);
|
|
13967
|
+
}
|
|
13814
13968
|
this._applyHintsToSources(opts.hints, opts.label);
|
|
13815
13969
|
const labelTag = opts.label ? ` [${opts.label}]` : "";
|
|
13816
13970
|
this.log(
|
|
@@ -13863,6 +14017,16 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13863
14017
|
}
|
|
13864
14018
|
return { length: q.length, dequeueCount: q.dequeueCount, cards };
|
|
13865
14019
|
};
|
|
14020
|
+
const drawnCards = this._sessionRecord.map((r) => {
|
|
14021
|
+
const last = r.records[r.records.length - 1];
|
|
14022
|
+
return {
|
|
14023
|
+
cardID: r.item.cardID,
|
|
14024
|
+
status: r.item.status,
|
|
14025
|
+
attempts: r.records.length,
|
|
14026
|
+
correct: last && isQuestionRecord(last) ? last.isCorrect : null,
|
|
14027
|
+
timeSpentMs: r.records.reduce((sum, rec) => sum + rec.timeSpent, 0)
|
|
14028
|
+
};
|
|
14029
|
+
});
|
|
13866
14030
|
return {
|
|
13867
14031
|
secondsRemaining: this.secondsRemaining,
|
|
13868
14032
|
hasCardGuarantee: this.hasCardGuarantee,
|
|
@@ -13874,7 +14038,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13874
14038
|
replanLabel: this._activeReplanLabel,
|
|
13875
14039
|
reviewQ: describe(this.reviewQ),
|
|
13876
14040
|
newQ: describe(this.newQ),
|
|
13877
|
-
failedQ: describe(this.failedQ)
|
|
14041
|
+
failedQ: describe(this.failedQ),
|
|
14042
|
+
drawnCards
|
|
13878
14043
|
};
|
|
13879
14044
|
}
|
|
13880
14045
|
/**
|
|
@@ -14183,7 +14348,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14183
14348
|
mixedWeighted
|
|
14184
14349
|
);
|
|
14185
14350
|
const reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review").slice(0, this._initialReviewCap);
|
|
14186
|
-
const newWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "new").slice(0, newLimit);
|
|
14351
|
+
const newWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)).slice(0, newLimit);
|
|
14187
14352
|
logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
|
|
14188
14353
|
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
14189
14354
|
if (!replan) {
|
|
@@ -14320,7 +14485,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14320
14485
|
this.log(
|
|
14321
14486
|
`[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
|
|
14322
14487
|
);
|
|
14323
|
-
void this.requestReplan({ label: "auto:depletion" });
|
|
14488
|
+
void this.requestReplan({ label: "auto:depletion", mode: "merge" });
|
|
14324
14489
|
}
|
|
14325
14490
|
const REPLAN_BUFFER = 3;
|
|
14326
14491
|
if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
|
|
@@ -14461,6 +14626,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14461
14626
|
* Remove an item from its source queue after consumption by nextCard().
|
|
14462
14627
|
*/
|
|
14463
14628
|
removeItemFromQueue(item) {
|
|
14629
|
+
this._clearDurableRequirement(item.cardID);
|
|
14630
|
+
this._servedCardIds.add(item.cardID);
|
|
14464
14631
|
if (this.reviewQ.peek(0)?.cardID === item.cardID) {
|
|
14465
14632
|
this.reviewQ.dequeue((queueItem) => queueItem.cardID);
|
|
14466
14633
|
} else if (this.newQ.peek(0)?.cardID === item.cardID) {
|
|
@@ -14472,6 +14639,27 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14472
14639
|
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
14473
14640
|
}
|
|
14474
14641
|
}
|
|
14642
|
+
/**
|
|
14643
|
+
* Remove a satisfied card ID from the durable session-hint `requireCards`
|
|
14644
|
+
* list. Called when a card is consumed (see removeItemFromQueue). No-op if
|
|
14645
|
+
* the card was not a durable requirement.
|
|
14646
|
+
*
|
|
14647
|
+
* Matches literal IDs only: a glob/pattern requirement (which may stand for
|
|
14648
|
+
* several cards) is NOT considered satisfied by a single draw and is left in
|
|
14649
|
+
* place — durable patterns are the caller's responsibility, one-shot `hints`
|
|
14650
|
+
* remain the right tool for them.
|
|
14651
|
+
*/
|
|
14652
|
+
_clearDurableRequirement(cardID) {
|
|
14653
|
+
const req = this._sessionHints?.requireCards;
|
|
14654
|
+
if (!req || req.length === 0) return;
|
|
14655
|
+
const next = req.filter((id) => id !== cardID);
|
|
14656
|
+
if (next.length === req.length) return;
|
|
14657
|
+
this._sessionHints = {
|
|
14658
|
+
...this._sessionHints,
|
|
14659
|
+
requireCards: next.length > 0 ? next : void 0
|
|
14660
|
+
};
|
|
14661
|
+
this.log(`[Replan] Durable requirement satisfied & cleared on draw: ${cardID}`);
|
|
14662
|
+
}
|
|
14475
14663
|
/**
|
|
14476
14664
|
* End the session and record learning outcomes.
|
|
14477
14665
|
*
|