@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.d.cts
CHANGED
|
@@ -315,6 +315,21 @@ interface SessionQueueDebug {
|
|
|
315
315
|
/** cardIDs in queue order, head (next draw) first. */
|
|
316
316
|
cards: string[];
|
|
317
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* A card the learner has interacted with this session (one entry per card in
|
|
320
|
+
* the session record, regardless of which queue — if any — still holds it).
|
|
321
|
+
*/
|
|
322
|
+
interface SessionDrawnCardDebug {
|
|
323
|
+
cardID: string;
|
|
324
|
+
/** Queue status at draw time: 'new' | 'review' | 'failed-new' | 'failed-review'. */
|
|
325
|
+
status: string;
|
|
326
|
+
/** Number of CardRecords logged for this card this session (≥1). */
|
|
327
|
+
attempts: number;
|
|
328
|
+
/** Latest record's correctness; null for non-question (info) records. */
|
|
329
|
+
correct: boolean | null;
|
|
330
|
+
/** Total time spent across all of this card's records, in ms. */
|
|
331
|
+
timeSpentMs: number;
|
|
332
|
+
}
|
|
318
333
|
/** Live snapshot of the controller, read fresh on each overlay tick. */
|
|
319
334
|
interface SessionDebugSnapshot {
|
|
320
335
|
secondsRemaining: number;
|
|
@@ -332,6 +347,8 @@ interface SessionDebugSnapshot {
|
|
|
332
347
|
reviewQ: SessionQueueDebug;
|
|
333
348
|
newQ: SessionQueueDebug;
|
|
334
349
|
failedQ: SessionQueueDebug;
|
|
350
|
+
/** Every card the learner has interacted with this session, draw order. */
|
|
351
|
+
drawnCards: SessionDrawnCardDebug[];
|
|
335
352
|
}
|
|
336
353
|
|
|
337
354
|
/**
|
|
@@ -366,6 +383,21 @@ interface ReplanOptions {
|
|
|
366
383
|
* multiply, require/exclude lists concatenate).
|
|
367
384
|
*/
|
|
368
385
|
sessionHints?: ReplanHints;
|
|
386
|
+
/**
|
|
387
|
+
* Like `sessionHints`, but *merged* into the existing session-durable hints
|
|
388
|
+
* (via `mergeHints`) instead of replacing them. Use when emphasis should
|
|
389
|
+
* *accumulate* across replans rather than clobber — e.g. introducing a second
|
|
390
|
+
* concept mid-session must not wipe the first concept's boost, nor any
|
|
391
|
+
* `difficultyBooster`/`conceptBackoff` state on other concepts.
|
|
392
|
+
*
|
|
393
|
+
* Merge semantics (see `mergeHints`): boosts MULTIPLY, require/exclude lists
|
|
394
|
+
* concat-dedup. Re-emphasising the *same* tag therefore compounds — callers
|
|
395
|
+
* boosting a tag they may have already boosted should clamp at the call site.
|
|
396
|
+
*
|
|
397
|
+
* If both `sessionHints` and `mergeSessionHints` are supplied, the replace is
|
|
398
|
+
* applied first, then the merge — but they are normally mutually exclusive.
|
|
399
|
+
*/
|
|
400
|
+
mergeSessionHints?: ReplanHints;
|
|
369
401
|
/**
|
|
370
402
|
* Maximum number of new cards to return from the pipeline.
|
|
371
403
|
* Default: 20 (the standard session batch size).
|
|
@@ -549,6 +581,21 @@ declare class SessionController<TView = unknown> extends Loggable {
|
|
|
549
581
|
* recomputed per-run in `_runReplan` and would otherwise go stale.
|
|
550
582
|
*/
|
|
551
583
|
private _sessionHints;
|
|
584
|
+
/**
|
|
585
|
+
* Card IDs that have been *served* (drawn/consumed) this session. Populated
|
|
586
|
+
* at the single consumption choke-point (removeItemFromQueue), so it reflects
|
|
587
|
+
* a draw the instant it happens — earlier than `_sessionRecord`, which only
|
|
588
|
+
* lands once the card is *responded to*.
|
|
589
|
+
*
|
|
590
|
+
* Used to keep already-served cards out of newQ on every (re)plan: a `new`
|
|
591
|
+
* card shown once must never re-enter newQ this session. This is the general
|
|
592
|
+
* guard against re-presentation — including the case where a replan in flight
|
|
593
|
+
* captured a now-drawn card (e.g. a +INF require-injected follow-up the
|
|
594
|
+
* depletion prefetch grabbed just before it was drawn). Reviews/failed cards
|
|
595
|
+
* legitimately recur and are tracked by their own queues, so this only gates
|
|
596
|
+
* `new`-origin candidates.
|
|
597
|
+
*/
|
|
598
|
+
private _servedCardIds;
|
|
552
599
|
/**
|
|
553
600
|
* Consumer-supplied hooks invoked after each question response is processed.
|
|
554
601
|
* Seeded from constructor options (threaded from
|
|
@@ -863,6 +910,17 @@ declare class SessionController<TView = unknown> extends Loggable {
|
|
|
863
910
|
* Remove an item from its source queue after consumption by nextCard().
|
|
864
911
|
*/
|
|
865
912
|
private removeItemFromQueue;
|
|
913
|
+
/**
|
|
914
|
+
* Remove a satisfied card ID from the durable session-hint `requireCards`
|
|
915
|
+
* list. Called when a card is consumed (see removeItemFromQueue). No-op if
|
|
916
|
+
* the card was not a durable requirement.
|
|
917
|
+
*
|
|
918
|
+
* Matches literal IDs only: a glob/pattern requirement (which may stand for
|
|
919
|
+
* several cards) is NOT considered satisfied by a single draw and is left in
|
|
920
|
+
* place — durable patterns are the caller's responsibility, one-shot `hints`
|
|
921
|
+
* remain the right tool for them.
|
|
922
|
+
*/
|
|
923
|
+
private _clearDurableRequirement;
|
|
866
924
|
/**
|
|
867
925
|
* End the session and record learning outcomes.
|
|
868
926
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -315,6 +315,21 @@ interface SessionQueueDebug {
|
|
|
315
315
|
/** cardIDs in queue order, head (next draw) first. */
|
|
316
316
|
cards: string[];
|
|
317
317
|
}
|
|
318
|
+
/**
|
|
319
|
+
* A card the learner has interacted with this session (one entry per card in
|
|
320
|
+
* the session record, regardless of which queue — if any — still holds it).
|
|
321
|
+
*/
|
|
322
|
+
interface SessionDrawnCardDebug {
|
|
323
|
+
cardID: string;
|
|
324
|
+
/** Queue status at draw time: 'new' | 'review' | 'failed-new' | 'failed-review'. */
|
|
325
|
+
status: string;
|
|
326
|
+
/** Number of CardRecords logged for this card this session (≥1). */
|
|
327
|
+
attempts: number;
|
|
328
|
+
/** Latest record's correctness; null for non-question (info) records. */
|
|
329
|
+
correct: boolean | null;
|
|
330
|
+
/** Total time spent across all of this card's records, in ms. */
|
|
331
|
+
timeSpentMs: number;
|
|
332
|
+
}
|
|
318
333
|
/** Live snapshot of the controller, read fresh on each overlay tick. */
|
|
319
334
|
interface SessionDebugSnapshot {
|
|
320
335
|
secondsRemaining: number;
|
|
@@ -332,6 +347,8 @@ interface SessionDebugSnapshot {
|
|
|
332
347
|
reviewQ: SessionQueueDebug;
|
|
333
348
|
newQ: SessionQueueDebug;
|
|
334
349
|
failedQ: SessionQueueDebug;
|
|
350
|
+
/** Every card the learner has interacted with this session, draw order. */
|
|
351
|
+
drawnCards: SessionDrawnCardDebug[];
|
|
335
352
|
}
|
|
336
353
|
|
|
337
354
|
/**
|
|
@@ -366,6 +383,21 @@ interface ReplanOptions {
|
|
|
366
383
|
* multiply, require/exclude lists concatenate).
|
|
367
384
|
*/
|
|
368
385
|
sessionHints?: ReplanHints;
|
|
386
|
+
/**
|
|
387
|
+
* Like `sessionHints`, but *merged* into the existing session-durable hints
|
|
388
|
+
* (via `mergeHints`) instead of replacing them. Use when emphasis should
|
|
389
|
+
* *accumulate* across replans rather than clobber — e.g. introducing a second
|
|
390
|
+
* concept mid-session must not wipe the first concept's boost, nor any
|
|
391
|
+
* `difficultyBooster`/`conceptBackoff` state on other concepts.
|
|
392
|
+
*
|
|
393
|
+
* Merge semantics (see `mergeHints`): boosts MULTIPLY, require/exclude lists
|
|
394
|
+
* concat-dedup. Re-emphasising the *same* tag therefore compounds — callers
|
|
395
|
+
* boosting a tag they may have already boosted should clamp at the call site.
|
|
396
|
+
*
|
|
397
|
+
* If both `sessionHints` and `mergeSessionHints` are supplied, the replace is
|
|
398
|
+
* applied first, then the merge — but they are normally mutually exclusive.
|
|
399
|
+
*/
|
|
400
|
+
mergeSessionHints?: ReplanHints;
|
|
369
401
|
/**
|
|
370
402
|
* Maximum number of new cards to return from the pipeline.
|
|
371
403
|
* Default: 20 (the standard session batch size).
|
|
@@ -549,6 +581,21 @@ declare class SessionController<TView = unknown> extends Loggable {
|
|
|
549
581
|
* recomputed per-run in `_runReplan` and would otherwise go stale.
|
|
550
582
|
*/
|
|
551
583
|
private _sessionHints;
|
|
584
|
+
/**
|
|
585
|
+
* Card IDs that have been *served* (drawn/consumed) this session. Populated
|
|
586
|
+
* at the single consumption choke-point (removeItemFromQueue), so it reflects
|
|
587
|
+
* a draw the instant it happens — earlier than `_sessionRecord`, which only
|
|
588
|
+
* lands once the card is *responded to*.
|
|
589
|
+
*
|
|
590
|
+
* Used to keep already-served cards out of newQ on every (re)plan: a `new`
|
|
591
|
+
* card shown once must never re-enter newQ this session. This is the general
|
|
592
|
+
* guard against re-presentation — including the case where a replan in flight
|
|
593
|
+
* captured a now-drawn card (e.g. a +INF require-injected follow-up the
|
|
594
|
+
* depletion prefetch grabbed just before it was drawn). Reviews/failed cards
|
|
595
|
+
* legitimately recur and are tracked by their own queues, so this only gates
|
|
596
|
+
* `new`-origin candidates.
|
|
597
|
+
*/
|
|
598
|
+
private _servedCardIds;
|
|
552
599
|
/**
|
|
553
600
|
* Consumer-supplied hooks invoked after each question response is processed.
|
|
554
601
|
* Seeded from constructor options (threaded from
|
|
@@ -863,6 +910,17 @@ declare class SessionController<TView = unknown> extends Loggable {
|
|
|
863
910
|
* Remove an item from its source queue after consumption by nextCard().
|
|
864
911
|
*/
|
|
865
912
|
private removeItemFromQueue;
|
|
913
|
+
/**
|
|
914
|
+
* Remove a satisfied card ID from the durable session-hint `requireCards`
|
|
915
|
+
* list. Called when a card is consumed (see removeItemFromQueue). No-op if
|
|
916
|
+
* the card was not a durable requirement.
|
|
917
|
+
*
|
|
918
|
+
* Matches literal IDs only: a glob/pattern requirement (which may stand for
|
|
919
|
+
* several cards) is NOT considered satisfied by a single draw and is left in
|
|
920
|
+
* place — durable patterns are the caller's responsibility, one-shot `hints`
|
|
921
|
+
* remain the right tool for them.
|
|
922
|
+
*/
|
|
923
|
+
private _clearDurableRequirement;
|
|
866
924
|
/**
|
|
867
925
|
* End the session and record learning outcomes.
|
|
868
926
|
*
|
package/dist/index.js
CHANGED
|
@@ -1939,13 +1939,14 @@ var elo_exports = {};
|
|
|
1939
1939
|
__export(elo_exports, {
|
|
1940
1940
|
default: () => ELONavigator
|
|
1941
1941
|
});
|
|
1942
|
-
var import_common5, ELONavigator;
|
|
1942
|
+
var import_common5, ELO_RELEVANCE_SIGMA, ELONavigator;
|
|
1943
1943
|
var init_elo = __esm({
|
|
1944
1944
|
"src/core/navigators/generators/elo.ts"() {
|
|
1945
1945
|
"use strict";
|
|
1946
1946
|
init_navigators();
|
|
1947
1947
|
import_common5 = require("@vue-skuilder/common");
|
|
1948
1948
|
init_logger();
|
|
1949
|
+
ELO_RELEVANCE_SIGMA = 300;
|
|
1949
1950
|
ELONavigator = class extends ContentNavigator {
|
|
1950
1951
|
/** Human-readable name for CardGenerator interface */
|
|
1951
1952
|
name;
|
|
@@ -1985,8 +1986,8 @@ var init_elo = __esm({
|
|
|
1985
1986
|
const scored = newCards.map((c) => {
|
|
1986
1987
|
const cardElo = c.elo ?? 1e3;
|
|
1987
1988
|
const distance = Math.abs(cardElo - userGlobalElo);
|
|
1988
|
-
const
|
|
1989
|
-
const samplingKey =
|
|
1989
|
+
const relevance = Math.exp(-((distance / ELO_RELEVANCE_SIGMA) ** 2));
|
|
1990
|
+
const samplingKey = relevance * (0.5 + 0.5 * Math.random());
|
|
1990
1991
|
return {
|
|
1991
1992
|
cardId: c.cardID,
|
|
1992
1993
|
courseId: c.courseID,
|
|
@@ -1998,7 +1999,7 @@ var init_elo = __esm({
|
|
|
1998
1999
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-ELO-default",
|
|
1999
2000
|
action: "generated",
|
|
2000
2001
|
score: samplingKey,
|
|
2001
|
-
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}),
|
|
2002
|
+
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), relevance ${relevance.toFixed(3)}, key ${samplingKey.toFixed(3)}`
|
|
2002
2003
|
}
|
|
2003
2004
|
]
|
|
2004
2005
|
};
|
|
@@ -13173,7 +13174,15 @@ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834"
|
|
|
13173
13174
|
var spinnerFrame = 0;
|
|
13174
13175
|
var overlayEl = null;
|
|
13175
13176
|
var pollHandle = null;
|
|
13176
|
-
var
|
|
13177
|
+
var lastSnapshot = null;
|
|
13178
|
+
var copyFlashUntil = 0;
|
|
13179
|
+
var minified = false;
|
|
13180
|
+
var expanded = {
|
|
13181
|
+
reviewQ: false,
|
|
13182
|
+
newQ: false,
|
|
13183
|
+
failedQ: false,
|
|
13184
|
+
drawn: false
|
|
13185
|
+
};
|
|
13177
13186
|
function toggleSessionOverlay() {
|
|
13178
13187
|
if (typeof document === "undefined") {
|
|
13179
13188
|
logger.info("[Session Overlay] No DOM available (non-browser host); overlay unavailable.");
|
|
@@ -13188,6 +13197,7 @@ function toggleSessionOverlay() {
|
|
|
13188
13197
|
}
|
|
13189
13198
|
}
|
|
13190
13199
|
function mount() {
|
|
13200
|
+
minified = false;
|
|
13191
13201
|
overlayEl = document.createElement("div");
|
|
13192
13202
|
overlayEl.id = OVERLAY_ID;
|
|
13193
13203
|
Object.assign(overlayEl.style, {
|
|
@@ -13226,11 +13236,23 @@ function render() {
|
|
|
13226
13236
|
spinnerFrame++;
|
|
13227
13237
|
const ctrl = getActiveController();
|
|
13228
13238
|
if (!ctrl) {
|
|
13229
|
-
|
|
13239
|
+
lastSnapshot = null;
|
|
13240
|
+
overlayEl.innerHTML = headerHtml() + (minified ? "" : `<div style="opacity:.65">No active session.</div>`);
|
|
13241
|
+
attachHandlers();
|
|
13230
13242
|
return;
|
|
13231
13243
|
}
|
|
13232
13244
|
const s = ctrl.getDebugSnapshot();
|
|
13233
|
-
|
|
13245
|
+
lastSnapshot = s;
|
|
13246
|
+
if (minified) {
|
|
13247
|
+
overlayEl.innerHTML = headerHtml();
|
|
13248
|
+
attachHandlers();
|
|
13249
|
+
return;
|
|
13250
|
+
}
|
|
13251
|
+
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);
|
|
13252
|
+
attachHandlers();
|
|
13253
|
+
}
|
|
13254
|
+
function attachHandlers() {
|
|
13255
|
+
if (!overlayEl) return;
|
|
13234
13256
|
overlayEl.querySelectorAll("[data-q]").forEach((el) => {
|
|
13235
13257
|
el.onclick = () => {
|
|
13236
13258
|
const key = el.dataset.q;
|
|
@@ -13239,9 +13261,45 @@ function render() {
|
|
|
13239
13261
|
render();
|
|
13240
13262
|
};
|
|
13241
13263
|
});
|
|
13264
|
+
const copyBtn = overlayEl.querySelector("[data-copy]");
|
|
13265
|
+
if (copyBtn) {
|
|
13266
|
+
copyBtn.onclick = (ev) => {
|
|
13267
|
+
ev.stopPropagation();
|
|
13268
|
+
copySnapshot();
|
|
13269
|
+
};
|
|
13270
|
+
}
|
|
13271
|
+
const minBtn = overlayEl.querySelector("[data-min]");
|
|
13272
|
+
if (minBtn) {
|
|
13273
|
+
minBtn.onclick = (ev) => {
|
|
13274
|
+
ev.stopPropagation();
|
|
13275
|
+
minified = !minified;
|
|
13276
|
+
render();
|
|
13277
|
+
};
|
|
13278
|
+
}
|
|
13279
|
+
}
|
|
13280
|
+
function copySnapshot() {
|
|
13281
|
+
const text = snapshotToText(lastSnapshot);
|
|
13282
|
+
const flash = () => {
|
|
13283
|
+
copyFlashUntil = Date.now() + 1200;
|
|
13284
|
+
render();
|
|
13285
|
+
};
|
|
13286
|
+
const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
13287
|
+
if (clip?.writeText) {
|
|
13288
|
+
clip.writeText(text).then(flash, (err) => {
|
|
13289
|
+
logger.warn(`[Session Overlay] Clipboard write failed: ${String(err)}`);
|
|
13290
|
+
});
|
|
13291
|
+
} else {
|
|
13292
|
+
logger.info(`[Session Overlay] Clipboard unavailable; snapshot follows:
|
|
13293
|
+
${text}`);
|
|
13294
|
+
}
|
|
13242
13295
|
}
|
|
13243
13296
|
function headerHtml() {
|
|
13244
|
-
|
|
13297
|
+
const flashing = Date.now() < copyFlashUntil;
|
|
13298
|
+
const btnLabel = flashing ? "\u2713 copied" : "\u2398 copy";
|
|
13299
|
+
const btnColor = flashing ? "#86efac" : "#93c5fd";
|
|
13300
|
+
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>`;
|
|
13301
|
+
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>`;
|
|
13302
|
+
return `<div style="font-weight:600;color:#93c5fd;margin-bottom:${minified ? 0 : 4}px">${copyBtn}${minBtn}\u2699 SessionController</div>`;
|
|
13245
13303
|
}
|
|
13246
13304
|
function replanHtml(s) {
|
|
13247
13305
|
if (!s.replanActive) {
|
|
@@ -13284,22 +13342,98 @@ function hintsHtml(h) {
|
|
|
13284
13342
|
}
|
|
13285
13343
|
function queueHtml(key, label, q) {
|
|
13286
13344
|
const collapsible = q.length > INLINE_THRESHOLD;
|
|
13287
|
-
const isOpen =
|
|
13345
|
+
const isOpen = collapsible && expanded[key];
|
|
13288
13346
|
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13289
13347
|
const drawn = q.dequeueCount ? ` <span style="opacity:.5">drawn ${q.dequeueCount}</span>` : "";
|
|
13290
13348
|
const titleStyle = collapsible ? "cursor:pointer;color:#f9a8d4" : "color:#f9a8d4";
|
|
13291
13349
|
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13292
13350
|
const title = `<div${titleAttr} style="${titleStyle}">${caret}${label}: ${q.length}${drawn}</div>`;
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13351
|
+
if (!q.cards.length) {
|
|
13352
|
+
return title + `<div style="margin:1px 0 6px 6px;opacity:.5">empty</div>`;
|
|
13353
|
+
}
|
|
13354
|
+
const shown = isOpen ? q.cards : q.cards.slice(0, INLINE_THRESHOLD);
|
|
13355
|
+
const hiddenCount = q.length - shown.length;
|
|
13356
|
+
const listMarginBottom = collapsible ? 2 : 6;
|
|
13357
|
+
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>`;
|
|
13358
|
+
if (collapsible) {
|
|
13359
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13360
|
+
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
13361
|
+
}
|
|
13362
|
+
return title + body;
|
|
13363
|
+
}
|
|
13364
|
+
function outcomeGlyph(correct) {
|
|
13365
|
+
if (correct === true) return `<span style="color:#86efac">\u2713</span>`;
|
|
13366
|
+
if (correct === false) return `<span style="color:#fca5a5">\u2717</span>`;
|
|
13367
|
+
return `<span style="opacity:.5">\xB7</span>`;
|
|
13368
|
+
}
|
|
13369
|
+
function drawnHtml(key, drawn) {
|
|
13370
|
+
const collapsible = drawn.length > INLINE_THRESHOLD;
|
|
13371
|
+
const isOpen = collapsible && expanded[key];
|
|
13372
|
+
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13373
|
+
const titleStyle = collapsible ? "cursor:pointer;color:#c4b5fd" : "color:#c4b5fd";
|
|
13374
|
+
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13375
|
+
const title = `<div${titleAttr} style="${titleStyle}">${caret}drawn: ${drawn.length}</div>`;
|
|
13376
|
+
if (!drawn.length) {
|
|
13377
|
+
return title + `<div style="margin:1px 0 6px 6px;opacity:.5">none yet</div>`;
|
|
13378
|
+
}
|
|
13379
|
+
const shown = isOpen ? drawn : drawn.slice(0, INLINE_THRESHOLD);
|
|
13380
|
+
const hiddenCount = drawn.length - shown.length;
|
|
13381
|
+
const listMarginBottom = collapsible ? 2 : 6;
|
|
13382
|
+
const rows = shown.map((d) => {
|
|
13383
|
+
const retries = d.attempts > 1 ? `<span style="opacity:.5"> \xD7${d.attempts}</span>` : "";
|
|
13384
|
+
const time = `<span style="opacity:.45"> ${Math.round(d.timeSpentMs / 100) / 10}s</span>`;
|
|
13385
|
+
return `<li style="white-space:nowrap">${outcomeGlyph(d.correct)} ${esc(d.cardID)}<span style="opacity:.5"> [${esc(d.status)}]</span>${retries}${time}</li>`;
|
|
13386
|
+
}).join("");
|
|
13387
|
+
let body = `<ol style="margin:2px 0 ${listMarginBottom}px 0;padding-left:20px">${rows}</ol>`;
|
|
13388
|
+
if (collapsible) {
|
|
13389
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13390
|
+
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
13300
13391
|
}
|
|
13301
13392
|
return title + body;
|
|
13302
13393
|
}
|
|
13394
|
+
function snapshotToText(s) {
|
|
13395
|
+
if (!s) return "SessionController \u2014 no active session.";
|
|
13396
|
+
const lines = [];
|
|
13397
|
+
lines.push("=== SessionController ===");
|
|
13398
|
+
lines.push(`time ${formatTime(s.secondsRemaining)}`);
|
|
13399
|
+
if (s.hasCardGuarantee) lines.push(`guarantee: ${s.minCardsGuarantee}`);
|
|
13400
|
+
lines.push(`well-indicated left: ${s.wellIndicatedRemaining}`);
|
|
13401
|
+
lines.push(`current: ${s.currentCard ?? "\u2014"}`);
|
|
13402
|
+
lines.push(
|
|
13403
|
+
s.replanActive ? `replan: ACTIVE [${s.replanLabel ?? "(auto)"}]` : "replan: idle"
|
|
13404
|
+
);
|
|
13405
|
+
lines.push("");
|
|
13406
|
+
lines.push("sessionHints:");
|
|
13407
|
+
const h = s.sessionHints;
|
|
13408
|
+
const hintParts = [];
|
|
13409
|
+
if (h) {
|
|
13410
|
+
if (h.boostTags && Object.keys(h.boostTags).length)
|
|
13411
|
+
hintParts.push(` boost: ${Object.entries(h.boostTags).map(([k, v]) => `${k}\xD7${v}`).join(", ")}`);
|
|
13412
|
+
if (h.boostCards && Object.keys(h.boostCards).length)
|
|
13413
|
+
hintParts.push(` boostCards: ${Object.entries(h.boostCards).map(([k, v]) => `${k}\xD7${v}`).join(", ")}`);
|
|
13414
|
+
if (h.requireCards?.length) hintParts.push(` require: ${h.requireCards.join(", ")}`);
|
|
13415
|
+
if (h.requireTags?.length) hintParts.push(` requireTags: ${h.requireTags.join(", ")}`);
|
|
13416
|
+
if (h.excludeTags?.length) hintParts.push(` exclude: ${h.excludeTags.join(", ")}`);
|
|
13417
|
+
if (h.excludeCards?.length) hintParts.push(` excludeCards: ${h.excludeCards.join(", ")}`);
|
|
13418
|
+
}
|
|
13419
|
+
lines.push(hintParts.length ? hintParts.join("\n") : " none");
|
|
13420
|
+
const queueText = (label, q) => {
|
|
13421
|
+
lines.push("");
|
|
13422
|
+
lines.push(`${label}: ${q.length} (drawn ${q.dequeueCount})`);
|
|
13423
|
+
q.cards.forEach((c, i) => lines.push(` ${i + 1}. ${c}`));
|
|
13424
|
+
};
|
|
13425
|
+
queueText("reviewQ", s.reviewQ);
|
|
13426
|
+
queueText("newQ", s.newQ);
|
|
13427
|
+
queueText("failedQ", s.failedQ);
|
|
13428
|
+
lines.push("");
|
|
13429
|
+
lines.push(`drawn: ${s.drawnCards.length}`);
|
|
13430
|
+
s.drawnCards.forEach((d, i) => {
|
|
13431
|
+
const mark = d.correct === true ? "\u2713" : d.correct === false ? "\u2717" : "\xB7";
|
|
13432
|
+
const time = `${Math.round(d.timeSpentMs / 100) / 10}s`;
|
|
13433
|
+
lines.push(` ${i + 1}. ${mark} ${d.cardID} [${d.status}] \xD7${d.attempts} ${time}`);
|
|
13434
|
+
});
|
|
13435
|
+
return lines.join("\n");
|
|
13436
|
+
}
|
|
13303
13437
|
function formatTime(totalSeconds) {
|
|
13304
13438
|
const s = Math.max(0, Math.round(totalSeconds));
|
|
13305
13439
|
const m = Math.floor(s / 60);
|
|
@@ -13669,6 +13803,21 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13669
13803
|
* recomputed per-run in `_runReplan` and would otherwise go stale.
|
|
13670
13804
|
*/
|
|
13671
13805
|
_sessionHints = null;
|
|
13806
|
+
/**
|
|
13807
|
+
* Card IDs that have been *served* (drawn/consumed) this session. Populated
|
|
13808
|
+
* at the single consumption choke-point (removeItemFromQueue), so it reflects
|
|
13809
|
+
* a draw the instant it happens — earlier than `_sessionRecord`, which only
|
|
13810
|
+
* lands once the card is *responded to*.
|
|
13811
|
+
*
|
|
13812
|
+
* Used to keep already-served cards out of newQ on every (re)plan: a `new`
|
|
13813
|
+
* card shown once must never re-enter newQ this session. This is the general
|
|
13814
|
+
* guard against re-presentation — including the case where a replan in flight
|
|
13815
|
+
* captured a now-drawn card (e.g. a +INF require-injected follow-up the
|
|
13816
|
+
* depletion prefetch grabbed just before it was drawn). Reviews/failed cards
|
|
13817
|
+
* legitimately recur and are tracked by their own queues, so this only gates
|
|
13818
|
+
* `new`-origin candidates.
|
|
13819
|
+
*/
|
|
13820
|
+
_servedCardIds = /* @__PURE__ */ new Set();
|
|
13672
13821
|
/**
|
|
13673
13822
|
* Consumer-supplied hooks invoked after each question response is processed.
|
|
13674
13823
|
* Seeded from constructor options (threaded from
|
|
@@ -13878,6 +14027,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13878
14027
|
if (opts.mode && opts.mode !== "replace") return true;
|
|
13879
14028
|
if (opts.hints && Object.keys(opts.hints).length > 0) return true;
|
|
13880
14029
|
if (opts.sessionHints !== void 0) return true;
|
|
14030
|
+
if (opts.mergeSessionHints !== void 0) return true;
|
|
13881
14031
|
return false;
|
|
13882
14032
|
}
|
|
13883
14033
|
/**
|
|
@@ -13913,6 +14063,10 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13913
14063
|
`[Replan] Session hints ${opts.sessionHints ? "set" : "cleared"}: ${JSON.stringify(opts.sessionHints)}`
|
|
13914
14064
|
);
|
|
13915
14065
|
}
|
|
14066
|
+
if (opts.mergeSessionHints !== void 0) {
|
|
14067
|
+
this._sessionHints = mergeHints2([this._sessionHints, opts.mergeSessionHints]) ?? null;
|
|
14068
|
+
this.log(`[Replan] Session hints merged: ${JSON.stringify(this._sessionHints)}`);
|
|
14069
|
+
}
|
|
13916
14070
|
this._applyHintsToSources(opts.hints, opts.label);
|
|
13917
14071
|
const labelTag = opts.label ? ` [${opts.label}]` : "";
|
|
13918
14072
|
this.log(
|
|
@@ -13965,6 +14119,16 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13965
14119
|
}
|
|
13966
14120
|
return { length: q.length, dequeueCount: q.dequeueCount, cards };
|
|
13967
14121
|
};
|
|
14122
|
+
const drawnCards = this._sessionRecord.map((r) => {
|
|
14123
|
+
const last = r.records[r.records.length - 1];
|
|
14124
|
+
return {
|
|
14125
|
+
cardID: r.item.cardID,
|
|
14126
|
+
status: r.item.status,
|
|
14127
|
+
attempts: r.records.length,
|
|
14128
|
+
correct: last && isQuestionRecord(last) ? last.isCorrect : null,
|
|
14129
|
+
timeSpentMs: r.records.reduce((sum, rec) => sum + rec.timeSpent, 0)
|
|
14130
|
+
};
|
|
14131
|
+
});
|
|
13968
14132
|
return {
|
|
13969
14133
|
secondsRemaining: this.secondsRemaining,
|
|
13970
14134
|
hasCardGuarantee: this.hasCardGuarantee,
|
|
@@ -13976,7 +14140,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13976
14140
|
replanLabel: this._activeReplanLabel,
|
|
13977
14141
|
reviewQ: describe(this.reviewQ),
|
|
13978
14142
|
newQ: describe(this.newQ),
|
|
13979
|
-
failedQ: describe(this.failedQ)
|
|
14143
|
+
failedQ: describe(this.failedQ),
|
|
14144
|
+
drawnCards
|
|
13980
14145
|
};
|
|
13981
14146
|
}
|
|
13982
14147
|
/**
|
|
@@ -14285,7 +14450,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14285
14450
|
mixedWeighted
|
|
14286
14451
|
);
|
|
14287
14452
|
const reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review").slice(0, this._initialReviewCap);
|
|
14288
|
-
const newWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "new").slice(0, newLimit);
|
|
14453
|
+
const newWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)).slice(0, newLimit);
|
|
14289
14454
|
logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
|
|
14290
14455
|
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
14291
14456
|
if (!replan) {
|
|
@@ -14422,7 +14587,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14422
14587
|
this.log(
|
|
14423
14588
|
`[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
|
|
14424
14589
|
);
|
|
14425
|
-
void this.requestReplan({ label: "auto:depletion" });
|
|
14590
|
+
void this.requestReplan({ label: "auto:depletion", mode: "merge" });
|
|
14426
14591
|
}
|
|
14427
14592
|
const REPLAN_BUFFER = 3;
|
|
14428
14593
|
if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
|
|
@@ -14563,6 +14728,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14563
14728
|
* Remove an item from its source queue after consumption by nextCard().
|
|
14564
14729
|
*/
|
|
14565
14730
|
removeItemFromQueue(item) {
|
|
14731
|
+
this._clearDurableRequirement(item.cardID);
|
|
14732
|
+
this._servedCardIds.add(item.cardID);
|
|
14566
14733
|
if (this.reviewQ.peek(0)?.cardID === item.cardID) {
|
|
14567
14734
|
this.reviewQ.dequeue((queueItem) => queueItem.cardID);
|
|
14568
14735
|
} else if (this.newQ.peek(0)?.cardID === item.cardID) {
|
|
@@ -14574,6 +14741,27 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14574
14741
|
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
14575
14742
|
}
|
|
14576
14743
|
}
|
|
14744
|
+
/**
|
|
14745
|
+
* Remove a satisfied card ID from the durable session-hint `requireCards`
|
|
14746
|
+
* list. Called when a card is consumed (see removeItemFromQueue). No-op if
|
|
14747
|
+
* the card was not a durable requirement.
|
|
14748
|
+
*
|
|
14749
|
+
* Matches literal IDs only: a glob/pattern requirement (which may stand for
|
|
14750
|
+
* several cards) is NOT considered satisfied by a single draw and is left in
|
|
14751
|
+
* place — durable patterns are the caller's responsibility, one-shot `hints`
|
|
14752
|
+
* remain the right tool for them.
|
|
14753
|
+
*/
|
|
14754
|
+
_clearDurableRequirement(cardID) {
|
|
14755
|
+
const req = this._sessionHints?.requireCards;
|
|
14756
|
+
if (!req || req.length === 0) return;
|
|
14757
|
+
const next = req.filter((id) => id !== cardID);
|
|
14758
|
+
if (next.length === req.length) return;
|
|
14759
|
+
this._sessionHints = {
|
|
14760
|
+
...this._sessionHints,
|
|
14761
|
+
requireCards: next.length > 0 ? next : void 0
|
|
14762
|
+
};
|
|
14763
|
+
this.log(`[Replan] Durable requirement satisfied & cleared on draw: ${cardID}`);
|
|
14764
|
+
}
|
|
14577
14765
|
/**
|
|
14578
14766
|
* End the session and record learning outcomes.
|
|
14579
14767
|
*
|