@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.js
CHANGED
|
@@ -4207,7 +4207,8 @@ var init_orchestration = __esm({
|
|
|
4207
4207
|
// src/core/navigators/Pipeline.ts
|
|
4208
4208
|
var Pipeline_exports = {};
|
|
4209
4209
|
__export(Pipeline_exports, {
|
|
4210
|
-
Pipeline: () => Pipeline
|
|
4210
|
+
Pipeline: () => Pipeline,
|
|
4211
|
+
mergeHints: () => mergeHints2
|
|
4211
4212
|
});
|
|
4212
4213
|
function globToRegex(pattern) {
|
|
4213
4214
|
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -11344,6 +11345,7 @@ var ItemQueue = class {
|
|
|
11344
11345
|
|
|
11345
11346
|
// src/study/SessionController.ts
|
|
11346
11347
|
init_couch();
|
|
11348
|
+
init_core();
|
|
11347
11349
|
init_recording();
|
|
11348
11350
|
|
|
11349
11351
|
// src/util/index.ts
|
|
@@ -12770,6 +12772,7 @@ init_dataDirectory();
|
|
|
12770
12772
|
|
|
12771
12773
|
// src/study/SessionController.ts
|
|
12772
12774
|
init_navigators();
|
|
12775
|
+
init_Pipeline();
|
|
12773
12776
|
|
|
12774
12777
|
// src/study/SourceMixer.ts
|
|
12775
12778
|
var QuotaRoundRobinMixer = class {
|
|
@@ -13153,6 +13156,161 @@ mountMixerDebugger();
|
|
|
13153
13156
|
// src/study/SessionDebugger.ts
|
|
13154
13157
|
init_logger();
|
|
13155
13158
|
init_PipelineDebugger();
|
|
13159
|
+
|
|
13160
|
+
// src/study/SessionOverlay.ts
|
|
13161
|
+
init_logger();
|
|
13162
|
+
var activeController = null;
|
|
13163
|
+
function registerActiveController(controller) {
|
|
13164
|
+
activeController = controller;
|
|
13165
|
+
}
|
|
13166
|
+
function getActiveController() {
|
|
13167
|
+
return activeController;
|
|
13168
|
+
}
|
|
13169
|
+
var OVERLAY_ID = "skuilder-session-overlay";
|
|
13170
|
+
var POLL_MS = 300;
|
|
13171
|
+
var INLINE_THRESHOLD = 5;
|
|
13172
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
13173
|
+
var spinnerFrame = 0;
|
|
13174
|
+
var overlayEl = null;
|
|
13175
|
+
var pollHandle = null;
|
|
13176
|
+
var expanded = { reviewQ: false, newQ: false, failedQ: false };
|
|
13177
|
+
function toggleSessionOverlay() {
|
|
13178
|
+
if (typeof document === "undefined") {
|
|
13179
|
+
logger.info("[Session Overlay] No DOM available (non-browser host); overlay unavailable.");
|
|
13180
|
+
return;
|
|
13181
|
+
}
|
|
13182
|
+
if (overlayEl) {
|
|
13183
|
+
teardown();
|
|
13184
|
+
logger.info("[Session Overlay] Hidden.");
|
|
13185
|
+
} else {
|
|
13186
|
+
mount();
|
|
13187
|
+
logger.info("[Session Overlay] Shown. Toggle off with window.skuilder.session.dbgOverlay().");
|
|
13188
|
+
}
|
|
13189
|
+
}
|
|
13190
|
+
function mount() {
|
|
13191
|
+
overlayEl = document.createElement("div");
|
|
13192
|
+
overlayEl.id = OVERLAY_ID;
|
|
13193
|
+
Object.assign(overlayEl.style, {
|
|
13194
|
+
position: "fixed",
|
|
13195
|
+
top: "8px",
|
|
13196
|
+
left: "8px",
|
|
13197
|
+
zIndex: "2147483647",
|
|
13198
|
+
maxWidth: "320px",
|
|
13199
|
+
maxHeight: "90vh",
|
|
13200
|
+
overflowY: "auto",
|
|
13201
|
+
padding: "8px 10px",
|
|
13202
|
+
background: "rgba(17, 24, 39, 0.92)",
|
|
13203
|
+
color: "#e5e7eb",
|
|
13204
|
+
font: "11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace",
|
|
13205
|
+
borderRadius: "6px",
|
|
13206
|
+
boxShadow: "0 4px 16px rgba(0,0,0,0.4)",
|
|
13207
|
+
pointerEvents: "auto",
|
|
13208
|
+
userSelect: "none"
|
|
13209
|
+
});
|
|
13210
|
+
document.body.appendChild(overlayEl);
|
|
13211
|
+
render();
|
|
13212
|
+
pollHandle = setInterval(render, POLL_MS);
|
|
13213
|
+
}
|
|
13214
|
+
function teardown() {
|
|
13215
|
+
if (pollHandle !== null) {
|
|
13216
|
+
clearInterval(pollHandle);
|
|
13217
|
+
pollHandle = null;
|
|
13218
|
+
}
|
|
13219
|
+
if (overlayEl?.parentNode) {
|
|
13220
|
+
overlayEl.parentNode.removeChild(overlayEl);
|
|
13221
|
+
}
|
|
13222
|
+
overlayEl = null;
|
|
13223
|
+
}
|
|
13224
|
+
function render() {
|
|
13225
|
+
if (!overlayEl) return;
|
|
13226
|
+
spinnerFrame++;
|
|
13227
|
+
const ctrl = getActiveController();
|
|
13228
|
+
if (!ctrl) {
|
|
13229
|
+
overlayEl.innerHTML = headerHtml() + `<div style="opacity:.65">No active session.</div>`;
|
|
13230
|
+
return;
|
|
13231
|
+
}
|
|
13232
|
+
const s = ctrl.getDebugSnapshot();
|
|
13233
|
+
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);
|
|
13234
|
+
overlayEl.querySelectorAll("[data-q]").forEach((el) => {
|
|
13235
|
+
el.onclick = () => {
|
|
13236
|
+
const key = el.dataset.q;
|
|
13237
|
+
if (!key) return;
|
|
13238
|
+
expanded[key] = !expanded[key];
|
|
13239
|
+
render();
|
|
13240
|
+
};
|
|
13241
|
+
});
|
|
13242
|
+
}
|
|
13243
|
+
function headerHtml() {
|
|
13244
|
+
return `<div style="font-weight:600;color:#93c5fd;margin-bottom:4px">\u2699 SessionController</div>`;
|
|
13245
|
+
}
|
|
13246
|
+
function replanHtml(s) {
|
|
13247
|
+
if (!s.replanActive) {
|
|
13248
|
+
return `<div style="margin-bottom:6px;opacity:.45">\u25CB idle</div>`;
|
|
13249
|
+
}
|
|
13250
|
+
const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
|
|
13251
|
+
const reason = esc(s.replanLabel ?? "(auto)");
|
|
13252
|
+
return `<div style="margin-bottom:6px;color:#fde047">${frame} replanning <span style="opacity:.85">[${reason}]</span></div>`;
|
|
13253
|
+
}
|
|
13254
|
+
function metaHtml(s) {
|
|
13255
|
+
const mmss = formatTime(s.secondsRemaining);
|
|
13256
|
+
const guarantee = s.hasCardGuarantee ? ` \xB7 <span style="color:#fbbf24">guarantee ${s.minCardsGuarantee}</span>` : "";
|
|
13257
|
+
const rows = [
|
|
13258
|
+
`time ${mmss}${guarantee}`,
|
|
13259
|
+
`well-indicated left: ${s.wellIndicatedRemaining}`,
|
|
13260
|
+
`current: ${s.currentCard ? esc(s.currentCard) : '<span style="opacity:.6">\u2014</span>'}`
|
|
13261
|
+
];
|
|
13262
|
+
return `<div style="margin-bottom:6px">${rows.map((r) => `<div>${r}</div>`).join("")}</div>`;
|
|
13263
|
+
}
|
|
13264
|
+
function hintsHtml(h) {
|
|
13265
|
+
const parts = [];
|
|
13266
|
+
if (h) {
|
|
13267
|
+
if (h.boostTags && Object.keys(h.boostTags).length) {
|
|
13268
|
+
parts.push(
|
|
13269
|
+
`boost: ` + Object.entries(h.boostTags).map(([k, v]) => `${esc(k)}<span style="opacity:.6">\xD7${v}</span>`).join(", ")
|
|
13270
|
+
);
|
|
13271
|
+
}
|
|
13272
|
+
if (h.boostCards && Object.keys(h.boostCards).length) {
|
|
13273
|
+
parts.push(
|
|
13274
|
+
`boostCards: ` + Object.entries(h.boostCards).map(([k, v]) => `${esc(k)}<span style="opacity:.6">\xD7${v}</span>`).join(", ")
|
|
13275
|
+
);
|
|
13276
|
+
}
|
|
13277
|
+
if (h.requireCards?.length) parts.push(`require: ${h.requireCards.map(esc).join(", ")}`);
|
|
13278
|
+
if (h.requireTags?.length) parts.push(`requireTags: ${h.requireTags.map(esc).join(", ")}`);
|
|
13279
|
+
if (h.excludeTags?.length) parts.push(`exclude: ${h.excludeTags.map(esc).join(", ")}`);
|
|
13280
|
+
if (h.excludeCards?.length) parts.push(`excludeCards: ${h.excludeCards.map(esc).join(", ")}`);
|
|
13281
|
+
}
|
|
13282
|
+
const body = parts.length ? parts.map((p) => `<div style="margin-left:6px">${p}</div>`).join("") : `<div style="margin-left:6px;opacity:.6">none</div>`;
|
|
13283
|
+
return `<div style="margin-bottom:6px"><div style="color:#86efac">sessionHints</div>${body}</div>`;
|
|
13284
|
+
}
|
|
13285
|
+
function queueHtml(key, label, q) {
|
|
13286
|
+
const collapsible = q.length > INLINE_THRESHOLD;
|
|
13287
|
+
const isOpen = !collapsible || expanded[key];
|
|
13288
|
+
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13289
|
+
const drawn = q.dequeueCount ? ` <span style="opacity:.5">drawn ${q.dequeueCount}</span>` : "";
|
|
13290
|
+
const titleStyle = collapsible ? "cursor:pointer;color:#f9a8d4" : "color:#f9a8d4";
|
|
13291
|
+
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13292
|
+
const title = `<div${titleAttr} style="${titleStyle}">${caret}${label}: ${q.length}${drawn}</div>`;
|
|
13293
|
+
let body = "";
|
|
13294
|
+
if (isOpen && q.cards.length) {
|
|
13295
|
+
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>`;
|
|
13296
|
+
} else if (!q.cards.length) {
|
|
13297
|
+
body = `<div style="margin:1px 0 6px 6px;opacity:.5">empty</div>`;
|
|
13298
|
+
} else {
|
|
13299
|
+
body = `<div style="margin:1px 0 6px 6px;opacity:.55">(${q.length} cards \u2014 click to expand)</div>`;
|
|
13300
|
+
}
|
|
13301
|
+
return title + body;
|
|
13302
|
+
}
|
|
13303
|
+
function formatTime(totalSeconds) {
|
|
13304
|
+
const s = Math.max(0, Math.round(totalSeconds));
|
|
13305
|
+
const m = Math.floor(s / 60);
|
|
13306
|
+
const r = s % 60;
|
|
13307
|
+
return `${m}:${r.toString().padStart(2, "0")}`;
|
|
13308
|
+
}
|
|
13309
|
+
function esc(value) {
|
|
13310
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
13311
|
+
}
|
|
13312
|
+
|
|
13313
|
+
// src/study/SessionDebugger.ts
|
|
13156
13314
|
var activeSession = null;
|
|
13157
13315
|
var sessionHistory = [];
|
|
13158
13316
|
var MAX_HISTORY = 5;
|
|
@@ -13333,6 +13491,13 @@ var sessionDebugAPI = {
|
|
|
13333
13491
|
showQueue() {
|
|
13334
13492
|
showCurrentQueue();
|
|
13335
13493
|
},
|
|
13494
|
+
/**
|
|
13495
|
+
* Toggle the pinned, live-updating DOM overlay for the active controller
|
|
13496
|
+
* (queues, session hints, timer). No-ops in non-browser hosts.
|
|
13497
|
+
*/
|
|
13498
|
+
dbgOverlay() {
|
|
13499
|
+
toggleSessionOverlay();
|
|
13500
|
+
},
|
|
13336
13501
|
/**
|
|
13337
13502
|
* Show presentation history for current or past session.
|
|
13338
13503
|
*/
|
|
@@ -13394,6 +13559,7 @@ var sessionDebugAPI = {
|
|
|
13394
13559
|
\u{1F3AF} Session Debug API
|
|
13395
13560
|
|
|
13396
13561
|
Commands:
|
|
13562
|
+
.dbgOverlay() Toggle the pinned live overlay (queues, hints, timer)
|
|
13397
13563
|
.showQueue() Show current queue state (active session only)
|
|
13398
13564
|
.showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
|
|
13399
13565
|
.showInterleaving(index?) Analyze course interleaving pattern
|
|
@@ -13462,6 +13628,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13462
13628
|
* Used by nextCard() to await completion before drawing from queues.
|
|
13463
13629
|
*/
|
|
13464
13630
|
_replanPromise = null;
|
|
13631
|
+
/**
|
|
13632
|
+
* Reason for the replan currently executing in `_runReplan`, surfaced by the
|
|
13633
|
+
* debug overlay's spinner. The caller's `opts.label` when present, else
|
|
13634
|
+
* `'(auto)'`. Only meaningful while `_replanPromise` is non-null; cleared
|
|
13635
|
+
* when the in-flight chain settles.
|
|
13636
|
+
*/
|
|
13637
|
+
_activeReplanLabel = null;
|
|
13465
13638
|
/**
|
|
13466
13639
|
* Number of well-indicated new cards remaining before the queue
|
|
13467
13640
|
* degrades to poorly-indicated content. Decremented on each newQ
|
|
@@ -13481,6 +13654,32 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13481
13654
|
* each nextCard() draw. Set by replans that include `minFollowUpCards`.
|
|
13482
13655
|
*/
|
|
13483
13656
|
_minCardsGuarantee = 0;
|
|
13657
|
+
/**
|
|
13658
|
+
* Session-durable scoring hints. Re-merged into every pipeline run for
|
|
13659
|
+
* the rest of the session (initial plan + every replan, including bare
|
|
13660
|
+
* auto-replans and the wedge-breaker), via `_applyHintsToSources`.
|
|
13661
|
+
*
|
|
13662
|
+
* Set by `setSessionHints()` (e.g. session-start post-lesson boost) or by
|
|
13663
|
+
* any replan carrying `ReplanOptions.sessionHints` (e.g. a just-failed
|
|
13664
|
+
* concept boost). Replace semantics, no decay — lives until overwritten
|
|
13665
|
+
* or session end. See `ReplanOptions.sessionHints` for rationale.
|
|
13666
|
+
*
|
|
13667
|
+
* Note: the controller-managed auto-excludes (current card, session
|
|
13668
|
+
* record, imminent draw) are intentionally NOT folded in here — those are
|
|
13669
|
+
* recomputed per-run in `_runReplan` and would otherwise go stale.
|
|
13670
|
+
*/
|
|
13671
|
+
_sessionHints = null;
|
|
13672
|
+
/**
|
|
13673
|
+
* Consumer-supplied hooks invoked after each question response is processed.
|
|
13674
|
+
* Seeded from constructor options (threaded from
|
|
13675
|
+
* `StudySessionConfig.outcomeObservers`). See {@link OutcomeObserver}.
|
|
13676
|
+
*/
|
|
13677
|
+
_outcomeObservers = [];
|
|
13678
|
+
/**
|
|
13679
|
+
* Lazily-built, stable capability object handed to observers. Bound to
|
|
13680
|
+
* `this`; constructed once so observers can rely on referential identity.
|
|
13681
|
+
*/
|
|
13682
|
+
_sessionControls = null;
|
|
13484
13683
|
startTime;
|
|
13485
13684
|
endTime;
|
|
13486
13685
|
_secondsRemaining;
|
|
@@ -13540,11 +13739,15 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13540
13739
|
if (options?.initialReviewCap !== void 0) {
|
|
13541
13740
|
this._initialReviewCap = options.initialReviewCap;
|
|
13542
13741
|
}
|
|
13742
|
+
if (options?.outcomeObservers?.length) {
|
|
13743
|
+
this._outcomeObservers = [...options.outcomeObservers];
|
|
13744
|
+
}
|
|
13543
13745
|
this.log(`Session constructed:
|
|
13544
13746
|
startTime: ${this.startTime}
|
|
13545
13747
|
endTime: ${this.endTime}
|
|
13546
13748
|
defaultBatchLimit: ${this._defaultBatchLimit}
|
|
13547
13749
|
initialReviewCap: ${this._initialReviewCap}`);
|
|
13750
|
+
registerActiveController(this);
|
|
13548
13751
|
}
|
|
13549
13752
|
tick() {
|
|
13550
13753
|
this._secondsRemaining = Math.floor((this.endTime.valueOf() - Date.now()) / 1e3);
|
|
@@ -13645,15 +13848,23 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13645
13848
|
);
|
|
13646
13849
|
const inflight = this._replanPromise;
|
|
13647
13850
|
const queued = inflight.catch(() => void 0).then(() => this._runReplan(opts));
|
|
13648
|
-
|
|
13649
|
-
if (this._replanPromise ===
|
|
13851
|
+
const tracked2 = queued.finally(() => {
|
|
13852
|
+
if (this._replanPromise === tracked2) {
|
|
13853
|
+
this._replanPromise = null;
|
|
13854
|
+
this._activeReplanLabel = null;
|
|
13855
|
+
}
|
|
13650
13856
|
});
|
|
13857
|
+
this._replanPromise = tracked2;
|
|
13651
13858
|
return queued;
|
|
13652
13859
|
}
|
|
13653
13860
|
const run = this._runReplan(opts);
|
|
13654
|
-
|
|
13655
|
-
if (this._replanPromise ===
|
|
13861
|
+
const tracked = run.finally(() => {
|
|
13862
|
+
if (this._replanPromise === tracked) {
|
|
13863
|
+
this._replanPromise = null;
|
|
13864
|
+
this._activeReplanLabel = null;
|
|
13865
|
+
}
|
|
13656
13866
|
});
|
|
13867
|
+
this._replanPromise = tracked;
|
|
13657
13868
|
await run;
|
|
13658
13869
|
}
|
|
13659
13870
|
/**
|
|
@@ -13662,11 +13873,11 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13662
13873
|
* triggers in nextCard) return false and may coalesce.
|
|
13663
13874
|
*/
|
|
13664
13875
|
_replanHasIntent(opts) {
|
|
13665
|
-
if (opts.label) return true;
|
|
13666
13876
|
if (opts.limit !== void 0) return true;
|
|
13667
13877
|
if (opts.minFollowUpCards !== void 0) return true;
|
|
13668
13878
|
if (opts.mode && opts.mode !== "replace") return true;
|
|
13669
13879
|
if (opts.hints && Object.keys(opts.hints).length > 0) return true;
|
|
13880
|
+
if (opts.sessionHints !== void 0) return true;
|
|
13670
13881
|
return false;
|
|
13671
13882
|
}
|
|
13672
13883
|
/**
|
|
@@ -13682,6 +13893,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13682
13893
|
* newQ.peek(0) is the imminent draw we need to exclude.
|
|
13683
13894
|
*/
|
|
13684
13895
|
async _runReplan(opts) {
|
|
13896
|
+
this._activeReplanLabel = opts.label ?? "(auto)";
|
|
13685
13897
|
if (!opts.hints) opts.hints = {};
|
|
13686
13898
|
const hints = opts.hints;
|
|
13687
13899
|
const excludeSet = new Set(hints.excludeCards ?? []);
|
|
@@ -13695,12 +13907,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13695
13907
|
excludeSet.add(this.newQ.peek(0).cardID);
|
|
13696
13908
|
}
|
|
13697
13909
|
hints.excludeCards = [...excludeSet];
|
|
13698
|
-
if (opts.
|
|
13699
|
-
|
|
13700
|
-
|
|
13701
|
-
|
|
13702
|
-
|
|
13910
|
+
if (opts.sessionHints !== void 0) {
|
|
13911
|
+
this._sessionHints = opts.sessionHints;
|
|
13912
|
+
this.log(
|
|
13913
|
+
`[Replan] Session hints ${opts.sessionHints ? "set" : "cleared"}: ${JSON.stringify(opts.sessionHints)}`
|
|
13914
|
+
);
|
|
13703
13915
|
}
|
|
13916
|
+
this._applyHintsToSources(opts.hints, opts.label);
|
|
13704
13917
|
const labelTag = opts.label ? ` [${opts.label}]` : "";
|
|
13705
13918
|
this.log(
|
|
13706
13919
|
`Mid-session replan requested${labelTag} (limit: ${opts.limit ?? "default"}, mode: ${opts.mode ?? "replace"}${opts.hints ? ", with hints" : ""})`
|
|
@@ -13711,6 +13924,128 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13711
13924
|
}
|
|
13712
13925
|
await this._executeReplan(opts);
|
|
13713
13926
|
}
|
|
13927
|
+
/**
|
|
13928
|
+
* Set the session-durable scoring hints (replace semantics, no decay).
|
|
13929
|
+
*
|
|
13930
|
+
* Unlike a one-shot replan hint, these are re-merged into every pipeline
|
|
13931
|
+
* run for the rest of the session — including the initial plan when set
|
|
13932
|
+
* before `prepareSession()`, every replan, the bare auto-replans, and the
|
|
13933
|
+
* wedge-breaker. Pass `null` to clear.
|
|
13934
|
+
*
|
|
13935
|
+
* Typical callers:
|
|
13936
|
+
* - `StudySession` at session start, threading `StudySessionConfig.initHints`
|
|
13937
|
+
* (e.g. a post-lesson concept boost) — so the boost outlives the first
|
|
13938
|
+
* queue rebuild instead of being clobbered by the first auto-replan.
|
|
13939
|
+
* - A consumer view on a failure, boosting the just-failed concept tag.
|
|
13940
|
+
*
|
|
13941
|
+
* Does not itself trigger a replan; the next plan/replan picks it up.
|
|
13942
|
+
*/
|
|
13943
|
+
setSessionHints(hints) {
|
|
13944
|
+
this._sessionHints = hints;
|
|
13945
|
+
this.log(`Session hints ${hints ? "set" : "cleared"}: ${JSON.stringify(hints)}`);
|
|
13946
|
+
}
|
|
13947
|
+
/**
|
|
13948
|
+
* Read the current session-durable hints (for read-modify-write callers,
|
|
13949
|
+
* e.g. an outcome observer that clamps a compounding boost).
|
|
13950
|
+
*/
|
|
13951
|
+
getSessionHints() {
|
|
13952
|
+
return this._sessionHints;
|
|
13953
|
+
}
|
|
13954
|
+
/**
|
|
13955
|
+
* Live state snapshot for the debug overlay (window.skuilder.session
|
|
13956
|
+
* .dbgOverlay()). Reads directly from the private queues and hints, so it
|
|
13957
|
+
* always reflects the current moment — unlike the passive SessionDebugger
|
|
13958
|
+
* snapshots, which only capture what was explicitly pushed to them.
|
|
13959
|
+
*/
|
|
13960
|
+
getDebugSnapshot() {
|
|
13961
|
+
const describe = (q) => {
|
|
13962
|
+
const cards = [];
|
|
13963
|
+
for (let i = 0; i < q.length; i++) {
|
|
13964
|
+
cards.push(q.peek(i).cardID);
|
|
13965
|
+
}
|
|
13966
|
+
return { length: q.length, dequeueCount: q.dequeueCount, cards };
|
|
13967
|
+
};
|
|
13968
|
+
return {
|
|
13969
|
+
secondsRemaining: this.secondsRemaining,
|
|
13970
|
+
hasCardGuarantee: this.hasCardGuarantee,
|
|
13971
|
+
minCardsGuarantee: this._minCardsGuarantee,
|
|
13972
|
+
wellIndicatedRemaining: this._wellIndicatedRemaining,
|
|
13973
|
+
currentCard: this._currentCard?.item.cardID ?? null,
|
|
13974
|
+
sessionHints: this._sessionHints,
|
|
13975
|
+
replanActive: this._replanPromise !== null,
|
|
13976
|
+
replanLabel: this._activeReplanLabel,
|
|
13977
|
+
reviewQ: describe(this.reviewQ),
|
|
13978
|
+
newQ: describe(this.newQ),
|
|
13979
|
+
failedQ: describe(this.failedQ)
|
|
13980
|
+
};
|
|
13981
|
+
}
|
|
13982
|
+
/**
|
|
13983
|
+
* Merge `hints` into the durable session hints via the pipeline's
|
|
13984
|
+
* `mergeHints` (boosts multiply, require/exclude lists concat-dedup).
|
|
13985
|
+
* Convenience over get-then-set for the common additive case. Note the
|
|
13986
|
+
* multiplicative, no-decay semantics — clamp boost factors at the call
|
|
13987
|
+
* site if a repeatedly-emphasised tag could compound unboundedly.
|
|
13988
|
+
*/
|
|
13989
|
+
mergeSessionHints(hints) {
|
|
13990
|
+
this._sessionHints = mergeHints2([this._sessionHints, hints]) ?? null;
|
|
13991
|
+
this.log(`Session hints merged: ${JSON.stringify(this._sessionHints)}`);
|
|
13992
|
+
}
|
|
13993
|
+
/**
|
|
13994
|
+
* Merge the durable `_sessionHints` with this run's one-shot hints and
|
|
13995
|
+
* push the result to every source for consumption on the next pipeline
|
|
13996
|
+
* run. Centralised so the initial plan and all replan paths apply session
|
|
13997
|
+
* emphasis identically. No-op when there are no hints of either kind.
|
|
13998
|
+
*/
|
|
13999
|
+
_applyHintsToSources(oneShot, label) {
|
|
14000
|
+
const oneShotWithLabel = oneShot && label ? { ...oneShot, _label: label } : oneShot;
|
|
14001
|
+
const merged = mergeHints2([this._sessionHints, oneShotWithLabel]);
|
|
14002
|
+
if (!merged) return;
|
|
14003
|
+
for (const source of this.sources) {
|
|
14004
|
+
source.setEphemeralHints?.(merged);
|
|
14005
|
+
}
|
|
14006
|
+
}
|
|
14007
|
+
/**
|
|
14008
|
+
* Build (once) the stable capability object handed to outcome observers.
|
|
14009
|
+
* Methods are bound to `this`; the object identity is stable across calls
|
|
14010
|
+
* so observers may key off it.
|
|
14011
|
+
*/
|
|
14012
|
+
_getSessionControls() {
|
|
14013
|
+
if (!this._sessionControls) {
|
|
14014
|
+
this._sessionControls = {
|
|
14015
|
+
getSessionHints: () => this.getSessionHints(),
|
|
14016
|
+
setSessionHints: (h) => this.setSessionHints(h),
|
|
14017
|
+
mergeSessionHints: (h) => this.mergeSessionHints(h),
|
|
14018
|
+
requestReplan: (opts) => this.requestReplan(opts)
|
|
14019
|
+
};
|
|
14020
|
+
}
|
|
14021
|
+
return this._sessionControls;
|
|
14022
|
+
}
|
|
14023
|
+
/**
|
|
14024
|
+
* Notify registered outcome observers about a processed response.
|
|
14025
|
+
*
|
|
14026
|
+
* Only question records are surfaced (non-question dismisses are skipped).
|
|
14027
|
+
* Observers run after ELO/SRS are recorded and before navigation. Each is
|
|
14028
|
+
* awaited but isolated in try/catch — a throwing observer is logged and
|
|
14029
|
+
* skipped, never wedging the session. Keep observers cheap and `void` any
|
|
14030
|
+
* long work (e.g. a triggered replan) to avoid stalling the draw.
|
|
14031
|
+
*/
|
|
14032
|
+
async _notifyOutcomeObservers(record, currentCard, result) {
|
|
14033
|
+
if (this._outcomeObservers.length === 0) return;
|
|
14034
|
+
if (!isQuestionRecord(record)) return;
|
|
14035
|
+
const outcome = {
|
|
14036
|
+
record,
|
|
14037
|
+
card: currentCard.card,
|
|
14038
|
+
result
|
|
14039
|
+
};
|
|
14040
|
+
const controls = this._getSessionControls();
|
|
14041
|
+
for (const observer of this._outcomeObservers) {
|
|
14042
|
+
try {
|
|
14043
|
+
await observer(outcome, controls);
|
|
14044
|
+
} catch (e) {
|
|
14045
|
+
this.error("[OutcomeObserver] observer threw; ignoring", e);
|
|
14046
|
+
}
|
|
14047
|
+
}
|
|
14048
|
+
}
|
|
13714
14049
|
/**
|
|
13715
14050
|
* Run a replan, bypassing requestReplan()'s coalesce logic.
|
|
13716
14051
|
*
|
|
@@ -13725,9 +14060,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13725
14060
|
*/
|
|
13726
14061
|
async _replanUncoalesced(opts) {
|
|
13727
14062
|
const run = this._runReplan(opts);
|
|
13728
|
-
|
|
13729
|
-
if (this._replanPromise ===
|
|
14063
|
+
const tracked = run.finally(() => {
|
|
14064
|
+
if (this._replanPromise === tracked) {
|
|
14065
|
+
this._replanPromise = null;
|
|
14066
|
+
this._activeReplanLabel = null;
|
|
14067
|
+
}
|
|
13730
14068
|
});
|
|
14069
|
+
this._replanPromise = tracked;
|
|
13731
14070
|
await run;
|
|
13732
14071
|
}
|
|
13733
14072
|
/**
|
|
@@ -13738,7 +14077,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13738
14077
|
*/
|
|
13739
14078
|
normalizeReplanOptions(input) {
|
|
13740
14079
|
if (!input) return {};
|
|
13741
|
-
const replanKeys = ["hints", "limit", "mode", "label", "minFollowUpCards"];
|
|
14080
|
+
const replanKeys = ["hints", "sessionHints", "limit", "mode", "label", "minFollowUpCards"];
|
|
13742
14081
|
const inputKeys = Object.keys(input);
|
|
13743
14082
|
if (inputKeys.some((k) => replanKeys.includes(k))) {
|
|
13744
14083
|
return input;
|
|
@@ -13890,6 +14229,9 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13890
14229
|
const additive = options?.additive ?? false;
|
|
13891
14230
|
const newLimit = options?.limit ?? this._defaultBatchLimit;
|
|
13892
14231
|
const fetchLimit = replan ? newLimit : newLimit + this._initialReviewCap;
|
|
14232
|
+
if (!replan) {
|
|
14233
|
+
this._applyHintsToSources();
|
|
14234
|
+
}
|
|
13893
14235
|
const batches = [];
|
|
13894
14236
|
for (let i = 0; i < this.sources.length; i++) {
|
|
13895
14237
|
const source = this.sources[i];
|
|
@@ -14080,14 +14422,14 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14080
14422
|
this.log(
|
|
14081
14423
|
`[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
|
|
14082
14424
|
);
|
|
14083
|
-
void this.requestReplan();
|
|
14425
|
+
void this.requestReplan({ label: "auto:depletion" });
|
|
14084
14426
|
}
|
|
14085
14427
|
const REPLAN_BUFFER = 3;
|
|
14086
14428
|
if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
|
|
14087
14429
|
this.log(
|
|
14088
14430
|
`[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (newQ: ${this.newQ.length}). Triggering background replan.`
|
|
14089
14431
|
);
|
|
14090
|
-
void this.requestReplan();
|
|
14432
|
+
void this.requestReplan({ label: "auto:quality" });
|
|
14091
14433
|
}
|
|
14092
14434
|
if (this._secondsRemaining <= 0 && this.failedQ.length === 0 && this._minCardsGuarantee <= 0) {
|
|
14093
14435
|
this._currentCard = null;
|
|
@@ -14170,7 +14512,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14170
14512
|
const studySessionItem = {
|
|
14171
14513
|
...currentCard.item
|
|
14172
14514
|
};
|
|
14173
|
-
|
|
14515
|
+
const result = await this.services.response.processResponse(
|
|
14174
14516
|
cardRecord,
|
|
14175
14517
|
cardHistory,
|
|
14176
14518
|
studySessionItem,
|
|
@@ -14182,6 +14524,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14182
14524
|
maxSessionViews,
|
|
14183
14525
|
sessionViews
|
|
14184
14526
|
);
|
|
14527
|
+
await this._notifyOutcomeObservers(cardRecord, currentCard, result);
|
|
14528
|
+
return result;
|
|
14185
14529
|
}
|
|
14186
14530
|
dismissCurrentCard(action = "dismiss-success") {
|
|
14187
14531
|
if (this._currentCard) {
|