@vue-skuilder/db 0.2.2 → 0.2.4
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/{contentSource-Ht3N2f-y.d.ts → contentSource-Cplhv3bJ.d.ts} +1 -1
- package/dist/{contentSource-BMlMwSiG.d.cts → contentSource-kI9_jwTu.d.cts} +1 -1
- package/dist/core/index.d.cts +5 -5
- package/dist/core/index.d.ts +5 -5
- package/dist/core/index.js +2 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +2 -1
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BEqB8VBR.d.cts → dataLayerProvider-CiA2Rr0v.d.cts} +1 -1
- package/dist/{dataLayerProvider-DObSXjnf.d.ts → dataLayerProvider-DrBqOUa3.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +3 -3
- package/dist/impl/couch/index.d.ts +3 -3
- package/dist/impl/couch/index.js +2 -1
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2 -1
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +4 -4
- package/dist/impl/static/index.d.ts +4 -4
- package/dist/impl/static/index.js +2 -1
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +2 -1
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-BWvO-_rJ.d.ts → index-BLLT5BYE.d.ts} +1 -1
- package/dist/{index-Ba7hYbHj.d.cts → index-k9NFHpS1.d.cts} +1 -1
- package/dist/index.d.cts +209 -10
- package/dist/index.d.ts +209 -10
- package/dist/index.js +361 -17
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +361 -17
- package/dist/index.mjs.map +1 -1
- package/dist/{types-W8n-B6HG.d.cts → types-BFUa1pa3.d.cts} +1 -1
- package/dist/{types-CJrLM1Ew.d.ts → types-CHgpWQAY.d.ts} +1 -1
- package/dist/{types-legacy-JXDxinpU.d.cts → types-legacy-4tlwHnXo.d.cts} +1 -1
- package/dist/{types-legacy-JXDxinpU.d.ts → types-legacy-4tlwHnXo.d.ts} +1 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/package.json +3 -3
- package/src/core/navigators/Pipeline.ts +1 -1
- package/src/study/SessionController.ts +347 -22
- package/src/study/SessionDebugger.ts +10 -0
- package/src/study/SessionOverlay.ts +276 -0
package/dist/index.mjs
CHANGED
|
@@ -4184,7 +4184,8 @@ var init_orchestration = __esm({
|
|
|
4184
4184
|
// src/core/navigators/Pipeline.ts
|
|
4185
4185
|
var Pipeline_exports = {};
|
|
4186
4186
|
__export(Pipeline_exports, {
|
|
4187
|
-
Pipeline: () => Pipeline
|
|
4187
|
+
Pipeline: () => Pipeline,
|
|
4188
|
+
mergeHints: () => mergeHints2
|
|
4188
4189
|
});
|
|
4189
4190
|
import { toCourseElo as toCourseElo5 } from "@vue-skuilder/common";
|
|
4190
4191
|
function globToRegex(pattern) {
|
|
@@ -11242,6 +11243,7 @@ var ItemQueue = class {
|
|
|
11242
11243
|
|
|
11243
11244
|
// src/study/SessionController.ts
|
|
11244
11245
|
init_couch();
|
|
11246
|
+
init_core();
|
|
11245
11247
|
init_recording();
|
|
11246
11248
|
|
|
11247
11249
|
// src/util/index.ts
|
|
@@ -12668,6 +12670,7 @@ init_dataDirectory();
|
|
|
12668
12670
|
|
|
12669
12671
|
// src/study/SessionController.ts
|
|
12670
12672
|
init_navigators();
|
|
12673
|
+
init_Pipeline();
|
|
12671
12674
|
|
|
12672
12675
|
// src/study/SourceMixer.ts
|
|
12673
12676
|
var QuotaRoundRobinMixer = class {
|
|
@@ -13051,6 +13054,161 @@ mountMixerDebugger();
|
|
|
13051
13054
|
// src/study/SessionDebugger.ts
|
|
13052
13055
|
init_logger();
|
|
13053
13056
|
init_PipelineDebugger();
|
|
13057
|
+
|
|
13058
|
+
// src/study/SessionOverlay.ts
|
|
13059
|
+
init_logger();
|
|
13060
|
+
var activeController = null;
|
|
13061
|
+
function registerActiveController(controller) {
|
|
13062
|
+
activeController = controller;
|
|
13063
|
+
}
|
|
13064
|
+
function getActiveController() {
|
|
13065
|
+
return activeController;
|
|
13066
|
+
}
|
|
13067
|
+
var OVERLAY_ID = "skuilder-session-overlay";
|
|
13068
|
+
var POLL_MS = 300;
|
|
13069
|
+
var INLINE_THRESHOLD = 5;
|
|
13070
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
13071
|
+
var spinnerFrame = 0;
|
|
13072
|
+
var overlayEl = null;
|
|
13073
|
+
var pollHandle = null;
|
|
13074
|
+
var expanded = { reviewQ: false, newQ: false, failedQ: false };
|
|
13075
|
+
function toggleSessionOverlay() {
|
|
13076
|
+
if (typeof document === "undefined") {
|
|
13077
|
+
logger.info("[Session Overlay] No DOM available (non-browser host); overlay unavailable.");
|
|
13078
|
+
return;
|
|
13079
|
+
}
|
|
13080
|
+
if (overlayEl) {
|
|
13081
|
+
teardown();
|
|
13082
|
+
logger.info("[Session Overlay] Hidden.");
|
|
13083
|
+
} else {
|
|
13084
|
+
mount();
|
|
13085
|
+
logger.info("[Session Overlay] Shown. Toggle off with window.skuilder.session.dbgOverlay().");
|
|
13086
|
+
}
|
|
13087
|
+
}
|
|
13088
|
+
function mount() {
|
|
13089
|
+
overlayEl = document.createElement("div");
|
|
13090
|
+
overlayEl.id = OVERLAY_ID;
|
|
13091
|
+
Object.assign(overlayEl.style, {
|
|
13092
|
+
position: "fixed",
|
|
13093
|
+
top: "8px",
|
|
13094
|
+
left: "8px",
|
|
13095
|
+
zIndex: "2147483647",
|
|
13096
|
+
maxWidth: "320px",
|
|
13097
|
+
maxHeight: "90vh",
|
|
13098
|
+
overflowY: "auto",
|
|
13099
|
+
padding: "8px 10px",
|
|
13100
|
+
background: "rgba(17, 24, 39, 0.92)",
|
|
13101
|
+
color: "#e5e7eb",
|
|
13102
|
+
font: "11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
13103
|
+
borderRadius: "6px",
|
|
13104
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.4)",
|
|
13105
|
+
pointerEvents: "auto",
|
|
13106
|
+
userSelect: "none"
|
|
13107
|
+
});
|
|
13108
|
+
document.body.appendChild(overlayEl);
|
|
13109
|
+
render();
|
|
13110
|
+
pollHandle = setInterval(render, POLL_MS);
|
|
13111
|
+
}
|
|
13112
|
+
function teardown() {
|
|
13113
|
+
if (pollHandle !== null) {
|
|
13114
|
+
clearInterval(pollHandle);
|
|
13115
|
+
pollHandle = null;
|
|
13116
|
+
}
|
|
13117
|
+
if (overlayEl?.parentNode) {
|
|
13118
|
+
overlayEl.parentNode.removeChild(overlayEl);
|
|
13119
|
+
}
|
|
13120
|
+
overlayEl = null;
|
|
13121
|
+
}
|
|
13122
|
+
function render() {
|
|
13123
|
+
if (!overlayEl) return;
|
|
13124
|
+
spinnerFrame++;
|
|
13125
|
+
const ctrl = getActiveController();
|
|
13126
|
+
if (!ctrl) {
|
|
13127
|
+
overlayEl.innerHTML = headerHtml() + `<div style="opacity:.65">No active session.</div>`;
|
|
13128
|
+
return;
|
|
13129
|
+
}
|
|
13130
|
+
const s = ctrl.getDebugSnapshot();
|
|
13131
|
+
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);
|
|
13132
|
+
overlayEl.querySelectorAll("[data-q]").forEach((el) => {
|
|
13133
|
+
el.onclick = () => {
|
|
13134
|
+
const key = el.dataset.q;
|
|
13135
|
+
if (!key) return;
|
|
13136
|
+
expanded[key] = !expanded[key];
|
|
13137
|
+
render();
|
|
13138
|
+
};
|
|
13139
|
+
});
|
|
13140
|
+
}
|
|
13141
|
+
function headerHtml() {
|
|
13142
|
+
return `<div style="font-weight:600;color:#93c5fd;margin-bottom:4px">\u2699 SessionController</div>`;
|
|
13143
|
+
}
|
|
13144
|
+
function replanHtml(s) {
|
|
13145
|
+
if (!s.replanActive) {
|
|
13146
|
+
return `<div style="margin-bottom:6px;opacity:.45">\u25CB idle</div>`;
|
|
13147
|
+
}
|
|
13148
|
+
const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
|
|
13149
|
+
const reason = esc(s.replanLabel ?? "(auto)");
|
|
13150
|
+
return `<div style="margin-bottom:6px;color:#fde047">${frame} replanning <span style="opacity:.85">[${reason}]</span></div>`;
|
|
13151
|
+
}
|
|
13152
|
+
function metaHtml(s) {
|
|
13153
|
+
const mmss = formatTime(s.secondsRemaining);
|
|
13154
|
+
const guarantee = s.hasCardGuarantee ? ` \xB7 <span style="color:#fbbf24">guarantee ${s.minCardsGuarantee}</span>` : "";
|
|
13155
|
+
const rows = [
|
|
13156
|
+
`time ${mmss}${guarantee}`,
|
|
13157
|
+
`well-indicated left: ${s.wellIndicatedRemaining}`,
|
|
13158
|
+
`current: ${s.currentCard ? esc(s.currentCard) : '<span style="opacity:.6">\u2014</span>'}`
|
|
13159
|
+
];
|
|
13160
|
+
return `<div style="margin-bottom:6px">${rows.map((r) => `<div>${r}</div>`).join("")}</div>`;
|
|
13161
|
+
}
|
|
13162
|
+
function hintsHtml(h) {
|
|
13163
|
+
const parts = [];
|
|
13164
|
+
if (h) {
|
|
13165
|
+
if (h.boostTags && Object.keys(h.boostTags).length) {
|
|
13166
|
+
parts.push(
|
|
13167
|
+
`boost: ` + Object.entries(h.boostTags).map(([k, v]) => `${esc(k)}<span style="opacity:.6">\xD7${v}</span>`).join(", ")
|
|
13168
|
+
);
|
|
13169
|
+
}
|
|
13170
|
+
if (h.boostCards && Object.keys(h.boostCards).length) {
|
|
13171
|
+
parts.push(
|
|
13172
|
+
`boostCards: ` + Object.entries(h.boostCards).map(([k, v]) => `${esc(k)}<span style="opacity:.6">\xD7${v}</span>`).join(", ")
|
|
13173
|
+
);
|
|
13174
|
+
}
|
|
13175
|
+
if (h.requireCards?.length) parts.push(`require: ${h.requireCards.map(esc).join(", ")}`);
|
|
13176
|
+
if (h.requireTags?.length) parts.push(`requireTags: ${h.requireTags.map(esc).join(", ")}`);
|
|
13177
|
+
if (h.excludeTags?.length) parts.push(`exclude: ${h.excludeTags.map(esc).join(", ")}`);
|
|
13178
|
+
if (h.excludeCards?.length) parts.push(`excludeCards: ${h.excludeCards.map(esc).join(", ")}`);
|
|
13179
|
+
}
|
|
13180
|
+
const body = parts.length ? parts.map((p) => `<div style="margin-left:6px">${p}</div>`).join("") : `<div style="margin-left:6px;opacity:.6">none</div>`;
|
|
13181
|
+
return `<div style="margin-bottom:6px"><div style="color:#86efac">sessionHints</div>${body}</div>`;
|
|
13182
|
+
}
|
|
13183
|
+
function queueHtml(key, label, q) {
|
|
13184
|
+
const collapsible = q.length > INLINE_THRESHOLD;
|
|
13185
|
+
const isOpen = !collapsible || expanded[key];
|
|
13186
|
+
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13187
|
+
const drawn = q.dequeueCount ? ` <span style="opacity:.5">drawn ${q.dequeueCount}</span>` : "";
|
|
13188
|
+
const titleStyle = collapsible ? "cursor:pointer;color:#f9a8d4" : "color:#f9a8d4";
|
|
13189
|
+
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13190
|
+
const title = `<div${titleAttr} style="${titleStyle}">${caret}${label}: ${q.length}${drawn}</div>`;
|
|
13191
|
+
let body = "";
|
|
13192
|
+
if (isOpen && q.cards.length) {
|
|
13193
|
+
body = `<ol style="margin:2px 0 6px 0;padding-left:20px">` + q.cards.map((c) => `<li style="white-space:nowrap">${esc(c)}</li>`).join("") + `</ol>`;
|
|
13194
|
+
} else if (!q.cards.length) {
|
|
13195
|
+
body = `<div style="margin:1px 0 6px 6px;opacity:.5">empty</div>`;
|
|
13196
|
+
} else {
|
|
13197
|
+
body = `<div style="margin:1px 0 6px 6px;opacity:.55">(${q.length} cards \u2014 click to expand)</div>`;
|
|
13198
|
+
}
|
|
13199
|
+
return title + body;
|
|
13200
|
+
}
|
|
13201
|
+
function formatTime(totalSeconds) {
|
|
13202
|
+
const s = Math.max(0, Math.round(totalSeconds));
|
|
13203
|
+
const m = Math.floor(s / 60);
|
|
13204
|
+
const r = s % 60;
|
|
13205
|
+
return `${m}:${r.toString().padStart(2, "0")}`;
|
|
13206
|
+
}
|
|
13207
|
+
function esc(value) {
|
|
13208
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
13209
|
+
}
|
|
13210
|
+
|
|
13211
|
+
// src/study/SessionDebugger.ts
|
|
13054
13212
|
var activeSession = null;
|
|
13055
13213
|
var sessionHistory = [];
|
|
13056
13214
|
var MAX_HISTORY = 5;
|
|
@@ -13231,6 +13389,13 @@ var sessionDebugAPI = {
|
|
|
13231
13389
|
showQueue() {
|
|
13232
13390
|
showCurrentQueue();
|
|
13233
13391
|
},
|
|
13392
|
+
/**
|
|
13393
|
+
* Toggle the pinned, live-updating DOM overlay for the active controller
|
|
13394
|
+
* (queues, session hints, timer). No-ops in non-browser hosts.
|
|
13395
|
+
*/
|
|
13396
|
+
dbgOverlay() {
|
|
13397
|
+
toggleSessionOverlay();
|
|
13398
|
+
},
|
|
13234
13399
|
/**
|
|
13235
13400
|
* Show presentation history for current or past session.
|
|
13236
13401
|
*/
|
|
@@ -13292,6 +13457,7 @@ var sessionDebugAPI = {
|
|
|
13292
13457
|
\u{1F3AF} Session Debug API
|
|
13293
13458
|
|
|
13294
13459
|
Commands:
|
|
13460
|
+
.dbgOverlay() Toggle the pinned live overlay (queues, hints, timer)
|
|
13295
13461
|
.showQueue() Show current queue state (active session only)
|
|
13296
13462
|
.showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
|
|
13297
13463
|
.showInterleaving(index?) Analyze course interleaving pattern
|
|
@@ -13360,6 +13526,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13360
13526
|
* Used by nextCard() to await completion before drawing from queues.
|
|
13361
13527
|
*/
|
|
13362
13528
|
_replanPromise = null;
|
|
13529
|
+
/**
|
|
13530
|
+
* Reason for the replan currently executing in `_runReplan`, surfaced by the
|
|
13531
|
+
* debug overlay's spinner. The caller's `opts.label` when present, else
|
|
13532
|
+
* `'(auto)'`. Only meaningful while `_replanPromise` is non-null; cleared
|
|
13533
|
+
* when the in-flight chain settles.
|
|
13534
|
+
*/
|
|
13535
|
+
_activeReplanLabel = null;
|
|
13363
13536
|
/**
|
|
13364
13537
|
* Number of well-indicated new cards remaining before the queue
|
|
13365
13538
|
* degrades to poorly-indicated content. Decremented on each newQ
|
|
@@ -13379,6 +13552,32 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13379
13552
|
* each nextCard() draw. Set by replans that include `minFollowUpCards`.
|
|
13380
13553
|
*/
|
|
13381
13554
|
_minCardsGuarantee = 0;
|
|
13555
|
+
/**
|
|
13556
|
+
* Session-durable scoring hints. Re-merged into every pipeline run for
|
|
13557
|
+
* the rest of the session (initial plan + every replan, including bare
|
|
13558
|
+
* auto-replans and the wedge-breaker), via `_applyHintsToSources`.
|
|
13559
|
+
*
|
|
13560
|
+
* Set by `setSessionHints()` (e.g. session-start post-lesson boost) or by
|
|
13561
|
+
* any replan carrying `ReplanOptions.sessionHints` (e.g. a just-failed
|
|
13562
|
+
* concept boost). Replace semantics, no decay — lives until overwritten
|
|
13563
|
+
* or session end. See `ReplanOptions.sessionHints` for rationale.
|
|
13564
|
+
*
|
|
13565
|
+
* Note: the controller-managed auto-excludes (current card, session
|
|
13566
|
+
* record, imminent draw) are intentionally NOT folded in here — those are
|
|
13567
|
+
* recomputed per-run in `_runReplan` and would otherwise go stale.
|
|
13568
|
+
*/
|
|
13569
|
+
_sessionHints = null;
|
|
13570
|
+
/**
|
|
13571
|
+
* Consumer-supplied hooks invoked after each question response is processed.
|
|
13572
|
+
* Seeded from constructor options (threaded from
|
|
13573
|
+
* `StudySessionConfig.outcomeObservers`). See {@link OutcomeObserver}.
|
|
13574
|
+
*/
|
|
13575
|
+
_outcomeObservers = [];
|
|
13576
|
+
/**
|
|
13577
|
+
* Lazily-built, stable capability object handed to observers. Bound to
|
|
13578
|
+
* `this`; constructed once so observers can rely on referential identity.
|
|
13579
|
+
*/
|
|
13580
|
+
_sessionControls = null;
|
|
13382
13581
|
startTime;
|
|
13383
13582
|
endTime;
|
|
13384
13583
|
_secondsRemaining;
|
|
@@ -13438,11 +13637,15 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13438
13637
|
if (options?.initialReviewCap !== void 0) {
|
|
13439
13638
|
this._initialReviewCap = options.initialReviewCap;
|
|
13440
13639
|
}
|
|
13640
|
+
if (options?.outcomeObservers?.length) {
|
|
13641
|
+
this._outcomeObservers = [...options.outcomeObservers];
|
|
13642
|
+
}
|
|
13441
13643
|
this.log(`Session constructed:
|
|
13442
13644
|
startTime: ${this.startTime}
|
|
13443
13645
|
endTime: ${this.endTime}
|
|
13444
13646
|
defaultBatchLimit: ${this._defaultBatchLimit}
|
|
13445
13647
|
initialReviewCap: ${this._initialReviewCap}`);
|
|
13648
|
+
registerActiveController(this);
|
|
13446
13649
|
}
|
|
13447
13650
|
tick() {
|
|
13448
13651
|
this._secondsRemaining = Math.floor((this.endTime.valueOf() - Date.now()) / 1e3);
|
|
@@ -13543,15 +13746,23 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13543
13746
|
);
|
|
13544
13747
|
const inflight = this._replanPromise;
|
|
13545
13748
|
const queued = inflight.catch(() => void 0).then(() => this._runReplan(opts));
|
|
13546
|
-
|
|
13547
|
-
if (this._replanPromise ===
|
|
13749
|
+
const tracked2 = queued.finally(() => {
|
|
13750
|
+
if (this._replanPromise === tracked2) {
|
|
13751
|
+
this._replanPromise = null;
|
|
13752
|
+
this._activeReplanLabel = null;
|
|
13753
|
+
}
|
|
13548
13754
|
});
|
|
13755
|
+
this._replanPromise = tracked2;
|
|
13549
13756
|
return queued;
|
|
13550
13757
|
}
|
|
13551
13758
|
const run = this._runReplan(opts);
|
|
13552
|
-
|
|
13553
|
-
if (this._replanPromise ===
|
|
13759
|
+
const tracked = run.finally(() => {
|
|
13760
|
+
if (this._replanPromise === tracked) {
|
|
13761
|
+
this._replanPromise = null;
|
|
13762
|
+
this._activeReplanLabel = null;
|
|
13763
|
+
}
|
|
13554
13764
|
});
|
|
13765
|
+
this._replanPromise = tracked;
|
|
13555
13766
|
await run;
|
|
13556
13767
|
}
|
|
13557
13768
|
/**
|
|
@@ -13560,11 +13771,11 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13560
13771
|
* triggers in nextCard) return false and may coalesce.
|
|
13561
13772
|
*/
|
|
13562
13773
|
_replanHasIntent(opts) {
|
|
13563
|
-
if (opts.label) return true;
|
|
13564
13774
|
if (opts.limit !== void 0) return true;
|
|
13565
13775
|
if (opts.minFollowUpCards !== void 0) return true;
|
|
13566
13776
|
if (opts.mode && opts.mode !== "replace") return true;
|
|
13567
13777
|
if (opts.hints && Object.keys(opts.hints).length > 0) return true;
|
|
13778
|
+
if (opts.sessionHints !== void 0) return true;
|
|
13568
13779
|
return false;
|
|
13569
13780
|
}
|
|
13570
13781
|
/**
|
|
@@ -13580,6 +13791,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13580
13791
|
* newQ.peek(0) is the imminent draw we need to exclude.
|
|
13581
13792
|
*/
|
|
13582
13793
|
async _runReplan(opts) {
|
|
13794
|
+
this._activeReplanLabel = opts.label ?? "(auto)";
|
|
13583
13795
|
if (!opts.hints) opts.hints = {};
|
|
13584
13796
|
const hints = opts.hints;
|
|
13585
13797
|
const excludeSet = new Set(hints.excludeCards ?? []);
|
|
@@ -13593,12 +13805,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13593
13805
|
excludeSet.add(this.newQ.peek(0).cardID);
|
|
13594
13806
|
}
|
|
13595
13807
|
hints.excludeCards = [...excludeSet];
|
|
13596
|
-
if (opts.
|
|
13597
|
-
|
|
13598
|
-
|
|
13599
|
-
|
|
13600
|
-
|
|
13808
|
+
if (opts.sessionHints !== void 0) {
|
|
13809
|
+
this._sessionHints = opts.sessionHints;
|
|
13810
|
+
this.log(
|
|
13811
|
+
`[Replan] Session hints ${opts.sessionHints ? "set" : "cleared"}: ${JSON.stringify(opts.sessionHints)}`
|
|
13812
|
+
);
|
|
13601
13813
|
}
|
|
13814
|
+
this._applyHintsToSources(opts.hints, opts.label);
|
|
13602
13815
|
const labelTag = opts.label ? ` [${opts.label}]` : "";
|
|
13603
13816
|
this.log(
|
|
13604
13817
|
`Mid-session replan requested${labelTag} (limit: ${opts.limit ?? "default"}, mode: ${opts.mode ?? "replace"}${opts.hints ? ", with hints" : ""})`
|
|
@@ -13609,6 +13822,128 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13609
13822
|
}
|
|
13610
13823
|
await this._executeReplan(opts);
|
|
13611
13824
|
}
|
|
13825
|
+
/**
|
|
13826
|
+
* Set the session-durable scoring hints (replace semantics, no decay).
|
|
13827
|
+
*
|
|
13828
|
+
* Unlike a one-shot replan hint, these are re-merged into every pipeline
|
|
13829
|
+
* run for the rest of the session — including the initial plan when set
|
|
13830
|
+
* before `prepareSession()`, every replan, the bare auto-replans, and the
|
|
13831
|
+
* wedge-breaker. Pass `null` to clear.
|
|
13832
|
+
*
|
|
13833
|
+
* Typical callers:
|
|
13834
|
+
* - `StudySession` at session start, threading `StudySessionConfig.initHints`
|
|
13835
|
+
* (e.g. a post-lesson concept boost) — so the boost outlives the first
|
|
13836
|
+
* queue rebuild instead of being clobbered by the first auto-replan.
|
|
13837
|
+
* - A consumer view on a failure, boosting the just-failed concept tag.
|
|
13838
|
+
*
|
|
13839
|
+
* Does not itself trigger a replan; the next plan/replan picks it up.
|
|
13840
|
+
*/
|
|
13841
|
+
setSessionHints(hints) {
|
|
13842
|
+
this._sessionHints = hints;
|
|
13843
|
+
this.log(`Session hints ${hints ? "set" : "cleared"}: ${JSON.stringify(hints)}`);
|
|
13844
|
+
}
|
|
13845
|
+
/**
|
|
13846
|
+
* Read the current session-durable hints (for read-modify-write callers,
|
|
13847
|
+
* e.g. an outcome observer that clamps a compounding boost).
|
|
13848
|
+
*/
|
|
13849
|
+
getSessionHints() {
|
|
13850
|
+
return this._sessionHints;
|
|
13851
|
+
}
|
|
13852
|
+
/**
|
|
13853
|
+
* Live state snapshot for the debug overlay (window.skuilder.session
|
|
13854
|
+
* .dbgOverlay()). Reads directly from the private queues and hints, so it
|
|
13855
|
+
* always reflects the current moment — unlike the passive SessionDebugger
|
|
13856
|
+
* snapshots, which only capture what was explicitly pushed to them.
|
|
13857
|
+
*/
|
|
13858
|
+
getDebugSnapshot() {
|
|
13859
|
+
const describe = (q) => {
|
|
13860
|
+
const cards = [];
|
|
13861
|
+
for (let i = 0; i < q.length; i++) {
|
|
13862
|
+
cards.push(q.peek(i).cardID);
|
|
13863
|
+
}
|
|
13864
|
+
return { length: q.length, dequeueCount: q.dequeueCount, cards };
|
|
13865
|
+
};
|
|
13866
|
+
return {
|
|
13867
|
+
secondsRemaining: this.secondsRemaining,
|
|
13868
|
+
hasCardGuarantee: this.hasCardGuarantee,
|
|
13869
|
+
minCardsGuarantee: this._minCardsGuarantee,
|
|
13870
|
+
wellIndicatedRemaining: this._wellIndicatedRemaining,
|
|
13871
|
+
currentCard: this._currentCard?.item.cardID ?? null,
|
|
13872
|
+
sessionHints: this._sessionHints,
|
|
13873
|
+
replanActive: this._replanPromise !== null,
|
|
13874
|
+
replanLabel: this._activeReplanLabel,
|
|
13875
|
+
reviewQ: describe(this.reviewQ),
|
|
13876
|
+
newQ: describe(this.newQ),
|
|
13877
|
+
failedQ: describe(this.failedQ)
|
|
13878
|
+
};
|
|
13879
|
+
}
|
|
13880
|
+
/**
|
|
13881
|
+
* Merge `hints` into the durable session hints via the pipeline's
|
|
13882
|
+
* `mergeHints` (boosts multiply, require/exclude lists concat-dedup).
|
|
13883
|
+
* Convenience over get-then-set for the common additive case. Note the
|
|
13884
|
+
* multiplicative, no-decay semantics — clamp boost factors at the call
|
|
13885
|
+
* site if a repeatedly-emphasised tag could compound unboundedly.
|
|
13886
|
+
*/
|
|
13887
|
+
mergeSessionHints(hints) {
|
|
13888
|
+
this._sessionHints = mergeHints2([this._sessionHints, hints]) ?? null;
|
|
13889
|
+
this.log(`Session hints merged: ${JSON.stringify(this._sessionHints)}`);
|
|
13890
|
+
}
|
|
13891
|
+
/**
|
|
13892
|
+
* Merge the durable `_sessionHints` with this run's one-shot hints and
|
|
13893
|
+
* push the result to every source for consumption on the next pipeline
|
|
13894
|
+
* run. Centralised so the initial plan and all replan paths apply session
|
|
13895
|
+
* emphasis identically. No-op when there are no hints of either kind.
|
|
13896
|
+
*/
|
|
13897
|
+
_applyHintsToSources(oneShot, label) {
|
|
13898
|
+
const oneShotWithLabel = oneShot && label ? { ...oneShot, _label: label } : oneShot;
|
|
13899
|
+
const merged = mergeHints2([this._sessionHints, oneShotWithLabel]);
|
|
13900
|
+
if (!merged) return;
|
|
13901
|
+
for (const source of this.sources) {
|
|
13902
|
+
source.setEphemeralHints?.(merged);
|
|
13903
|
+
}
|
|
13904
|
+
}
|
|
13905
|
+
/**
|
|
13906
|
+
* Build (once) the stable capability object handed to outcome observers.
|
|
13907
|
+
* Methods are bound to `this`; the object identity is stable across calls
|
|
13908
|
+
* so observers may key off it.
|
|
13909
|
+
*/
|
|
13910
|
+
_getSessionControls() {
|
|
13911
|
+
if (!this._sessionControls) {
|
|
13912
|
+
this._sessionControls = {
|
|
13913
|
+
getSessionHints: () => this.getSessionHints(),
|
|
13914
|
+
setSessionHints: (h) => this.setSessionHints(h),
|
|
13915
|
+
mergeSessionHints: (h) => this.mergeSessionHints(h),
|
|
13916
|
+
requestReplan: (opts) => this.requestReplan(opts)
|
|
13917
|
+
};
|
|
13918
|
+
}
|
|
13919
|
+
return this._sessionControls;
|
|
13920
|
+
}
|
|
13921
|
+
/**
|
|
13922
|
+
* Notify registered outcome observers about a processed response.
|
|
13923
|
+
*
|
|
13924
|
+
* Only question records are surfaced (non-question dismisses are skipped).
|
|
13925
|
+
* Observers run after ELO/SRS are recorded and before navigation. Each is
|
|
13926
|
+
* awaited but isolated in try/catch — a throwing observer is logged and
|
|
13927
|
+
* skipped, never wedging the session. Keep observers cheap and `void` any
|
|
13928
|
+
* long work (e.g. a triggered replan) to avoid stalling the draw.
|
|
13929
|
+
*/
|
|
13930
|
+
async _notifyOutcomeObservers(record, currentCard, result) {
|
|
13931
|
+
if (this._outcomeObservers.length === 0) return;
|
|
13932
|
+
if (!isQuestionRecord(record)) return;
|
|
13933
|
+
const outcome = {
|
|
13934
|
+
record,
|
|
13935
|
+
card: currentCard.card,
|
|
13936
|
+
result
|
|
13937
|
+
};
|
|
13938
|
+
const controls = this._getSessionControls();
|
|
13939
|
+
for (const observer of this._outcomeObservers) {
|
|
13940
|
+
try {
|
|
13941
|
+
await observer(outcome, controls);
|
|
13942
|
+
} catch (e) {
|
|
13943
|
+
this.error("[OutcomeObserver] observer threw; ignoring", e);
|
|
13944
|
+
}
|
|
13945
|
+
}
|
|
13946
|
+
}
|
|
13612
13947
|
/**
|
|
13613
13948
|
* Run a replan, bypassing requestReplan()'s coalesce logic.
|
|
13614
13949
|
*
|
|
@@ -13623,9 +13958,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13623
13958
|
*/
|
|
13624
13959
|
async _replanUncoalesced(opts) {
|
|
13625
13960
|
const run = this._runReplan(opts);
|
|
13626
|
-
|
|
13627
|
-
if (this._replanPromise ===
|
|
13961
|
+
const tracked = run.finally(() => {
|
|
13962
|
+
if (this._replanPromise === tracked) {
|
|
13963
|
+
this._replanPromise = null;
|
|
13964
|
+
this._activeReplanLabel = null;
|
|
13965
|
+
}
|
|
13628
13966
|
});
|
|
13967
|
+
this._replanPromise = tracked;
|
|
13629
13968
|
await run;
|
|
13630
13969
|
}
|
|
13631
13970
|
/**
|
|
@@ -13636,7 +13975,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13636
13975
|
*/
|
|
13637
13976
|
normalizeReplanOptions(input) {
|
|
13638
13977
|
if (!input) return {};
|
|
13639
|
-
const replanKeys = ["hints", "limit", "mode", "label", "minFollowUpCards"];
|
|
13978
|
+
const replanKeys = ["hints", "sessionHints", "limit", "mode", "label", "minFollowUpCards"];
|
|
13640
13979
|
const inputKeys = Object.keys(input);
|
|
13641
13980
|
if (inputKeys.some((k) => replanKeys.includes(k))) {
|
|
13642
13981
|
return input;
|
|
@@ -13788,6 +14127,9 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13788
14127
|
const additive = options?.additive ?? false;
|
|
13789
14128
|
const newLimit = options?.limit ?? this._defaultBatchLimit;
|
|
13790
14129
|
const fetchLimit = replan ? newLimit : newLimit + this._initialReviewCap;
|
|
14130
|
+
if (!replan) {
|
|
14131
|
+
this._applyHintsToSources();
|
|
14132
|
+
}
|
|
13791
14133
|
const batches = [];
|
|
13792
14134
|
for (let i = 0; i < this.sources.length; i++) {
|
|
13793
14135
|
const source = this.sources[i];
|
|
@@ -13978,14 +14320,14 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13978
14320
|
this.log(
|
|
13979
14321
|
`[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
|
|
13980
14322
|
);
|
|
13981
|
-
void this.requestReplan();
|
|
14323
|
+
void this.requestReplan({ label: "auto:depletion" });
|
|
13982
14324
|
}
|
|
13983
14325
|
const REPLAN_BUFFER = 3;
|
|
13984
14326
|
if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
|
|
13985
14327
|
this.log(
|
|
13986
14328
|
`[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (newQ: ${this.newQ.length}). Triggering background replan.`
|
|
13987
14329
|
);
|
|
13988
|
-
void this.requestReplan();
|
|
14330
|
+
void this.requestReplan({ label: "auto:quality" });
|
|
13989
14331
|
}
|
|
13990
14332
|
if (this._secondsRemaining <= 0 && this.failedQ.length === 0 && this._minCardsGuarantee <= 0) {
|
|
13991
14333
|
this._currentCard = null;
|
|
@@ -14068,7 +14410,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14068
14410
|
const studySessionItem = {
|
|
14069
14411
|
...currentCard.item
|
|
14070
14412
|
};
|
|
14071
|
-
|
|
14413
|
+
const result = await this.services.response.processResponse(
|
|
14072
14414
|
cardRecord,
|
|
14073
14415
|
cardHistory,
|
|
14074
14416
|
studySessionItem,
|
|
@@ -14080,6 +14422,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14080
14422
|
maxSessionViews,
|
|
14081
14423
|
sessionViews
|
|
14082
14424
|
);
|
|
14425
|
+
await this._notifyOutcomeObservers(cardRecord, currentCard, result);
|
|
14426
|
+
return result;
|
|
14083
14427
|
}
|
|
14084
14428
|
dismissCurrentCard(action = "dismiss-success") {
|
|
14085
14429
|
if (this._currentCard) {
|