@vue-skuilder/db 0.2.3 → 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/index.d.cts CHANGED
@@ -308,6 +308,32 @@ declare class QuotaRoundRobinMixer implements SourceMixer {
308
308
  mix(batches: SourceBatch[], limit: number): WeightedCard[];
309
309
  }
310
310
 
311
+ /** Per-queue debug view: total length, cumulative draws, and head-first cardIDs. */
312
+ interface SessionQueueDebug {
313
+ length: number;
314
+ dequeueCount: number;
315
+ /** cardIDs in queue order, head (next draw) first. */
316
+ cards: string[];
317
+ }
318
+ /** Live snapshot of the controller, read fresh on each overlay tick. */
319
+ interface SessionDebugSnapshot {
320
+ secondsRemaining: number;
321
+ hasCardGuarantee: boolean;
322
+ minCardsGuarantee: number;
323
+ wellIndicatedRemaining: number;
324
+ /** cardID of the card currently in front of the learner, if any. */
325
+ currentCard: string | null;
326
+ /** Session-durable hints re-merged into every pipeline run this session. */
327
+ sessionHints: ReplanHints | null;
328
+ /** True while a replan is executing (in-flight). */
329
+ replanActive: boolean;
330
+ /** Reason for the in-flight replan (caller label, or '(auto)'); may be stale when idle. */
331
+ replanLabel: string | null;
332
+ reviewQ: SessionQueueDebug;
333
+ newQ: SessionQueueDebug;
334
+ failedQ: SessionQueueDebug;
335
+ }
336
+
311
337
  /**
312
338
  * Options for requesting a mid-session replan.
313
339
  *
@@ -482,6 +508,13 @@ declare class SessionController<TView = unknown> extends Loggable {
482
508
  * Used by nextCard() to await completion before drawing from queues.
483
509
  */
484
510
  private _replanPromise;
511
+ /**
512
+ * Reason for the replan currently executing in `_runReplan`, surfaced by the
513
+ * debug overlay's spinner. The caller's `opts.label` when present, else
514
+ * `'(auto)'`. Only meaningful while `_replanPromise` is non-null; cleared
515
+ * when the in-flight chain settles.
516
+ */
517
+ private _activeReplanLabel;
485
518
  /**
486
519
  * Number of well-indicated new cards remaining before the queue
487
520
  * degrades to poorly-indicated content. Decremented on each newQ
@@ -637,6 +670,13 @@ declare class SessionController<TView = unknown> extends Loggable {
637
670
  * e.g. an outcome observer that clamps a compounding boost).
638
671
  */
639
672
  getSessionHints(): ReplanHints | null;
673
+ /**
674
+ * Live state snapshot for the debug overlay (window.skuilder.session
675
+ * .dbgOverlay()). Reads directly from the private queues and hints, so it
676
+ * always reflects the current moment — unlike the passive SessionDebugger
677
+ * snapshots, which only capture what was explicitly pushed to them.
678
+ */
679
+ getDebugSnapshot(): SessionDebugSnapshot;
640
680
  /**
641
681
  * Merge `hints` into the durable session hints via the pipeline's
642
682
  * `mergeHints` (boosts multiply, require/exclude lists concat-dedup).
@@ -1060,6 +1100,11 @@ declare const sessionDebugAPI: {
1060
1100
  * Show current queue state.
1061
1101
  */
1062
1102
  showQueue(): void;
1103
+ /**
1104
+ * Toggle the pinned, live-updating DOM overlay for the active controller
1105
+ * (queues, session hints, timer). No-ops in non-browser hosts.
1106
+ */
1107
+ dbgOverlay(): void;
1063
1108
  /**
1064
1109
  * Show presentation history for current or past session.
1065
1110
  */
package/dist/index.d.ts CHANGED
@@ -308,6 +308,32 @@ declare class QuotaRoundRobinMixer implements SourceMixer {
308
308
  mix(batches: SourceBatch[], limit: number): WeightedCard[];
309
309
  }
310
310
 
311
+ /** Per-queue debug view: total length, cumulative draws, and head-first cardIDs. */
312
+ interface SessionQueueDebug {
313
+ length: number;
314
+ dequeueCount: number;
315
+ /** cardIDs in queue order, head (next draw) first. */
316
+ cards: string[];
317
+ }
318
+ /** Live snapshot of the controller, read fresh on each overlay tick. */
319
+ interface SessionDebugSnapshot {
320
+ secondsRemaining: number;
321
+ hasCardGuarantee: boolean;
322
+ minCardsGuarantee: number;
323
+ wellIndicatedRemaining: number;
324
+ /** cardID of the card currently in front of the learner, if any. */
325
+ currentCard: string | null;
326
+ /** Session-durable hints re-merged into every pipeline run this session. */
327
+ sessionHints: ReplanHints | null;
328
+ /** True while a replan is executing (in-flight). */
329
+ replanActive: boolean;
330
+ /** Reason for the in-flight replan (caller label, or '(auto)'); may be stale when idle. */
331
+ replanLabel: string | null;
332
+ reviewQ: SessionQueueDebug;
333
+ newQ: SessionQueueDebug;
334
+ failedQ: SessionQueueDebug;
335
+ }
336
+
311
337
  /**
312
338
  * Options for requesting a mid-session replan.
313
339
  *
@@ -482,6 +508,13 @@ declare class SessionController<TView = unknown> extends Loggable {
482
508
  * Used by nextCard() to await completion before drawing from queues.
483
509
  */
484
510
  private _replanPromise;
511
+ /**
512
+ * Reason for the replan currently executing in `_runReplan`, surfaced by the
513
+ * debug overlay's spinner. The caller's `opts.label` when present, else
514
+ * `'(auto)'`. Only meaningful while `_replanPromise` is non-null; cleared
515
+ * when the in-flight chain settles.
516
+ */
517
+ private _activeReplanLabel;
485
518
  /**
486
519
  * Number of well-indicated new cards remaining before the queue
487
520
  * degrades to poorly-indicated content. Decremented on each newQ
@@ -637,6 +670,13 @@ declare class SessionController<TView = unknown> extends Loggable {
637
670
  * e.g. an outcome observer that clamps a compounding boost).
638
671
  */
639
672
  getSessionHints(): ReplanHints | null;
673
+ /**
674
+ * Live state snapshot for the debug overlay (window.skuilder.session
675
+ * .dbgOverlay()). Reads directly from the private queues and hints, so it
676
+ * always reflects the current moment — unlike the passive SessionDebugger
677
+ * snapshots, which only capture what was explicitly pushed to them.
678
+ */
679
+ getDebugSnapshot(): SessionDebugSnapshot;
640
680
  /**
641
681
  * Merge `hints` into the durable session hints via the pipeline's
642
682
  * `mergeHints` (boosts multiply, require/exclude lists concat-dedup).
@@ -1060,6 +1100,11 @@ declare const sessionDebugAPI: {
1060
1100
  * Show current queue state.
1061
1101
  */
1062
1102
  showQueue(): void;
1103
+ /**
1104
+ * Toggle the pinned, live-updating DOM overlay for the active controller
1105
+ * (queues, session hints, timer). No-ops in non-browser hosts.
1106
+ */
1107
+ dbgOverlay(): void;
1063
1108
  /**
1064
1109
  * Show presentation history for current or past session.
1065
1110
  */
package/dist/index.js CHANGED
@@ -13156,6 +13156,161 @@ mountMixerDebugger();
13156
13156
  // src/study/SessionDebugger.ts
13157
13157
  init_logger();
13158
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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
13311
+ }
13312
+
13313
+ // src/study/SessionDebugger.ts
13159
13314
  var activeSession = null;
13160
13315
  var sessionHistory = [];
13161
13316
  var MAX_HISTORY = 5;
@@ -13336,6 +13491,13 @@ var sessionDebugAPI = {
13336
13491
  showQueue() {
13337
13492
  showCurrentQueue();
13338
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
+ },
13339
13501
  /**
13340
13502
  * Show presentation history for current or past session.
13341
13503
  */
@@ -13397,6 +13559,7 @@ var sessionDebugAPI = {
13397
13559
  \u{1F3AF} Session Debug API
13398
13560
 
13399
13561
  Commands:
13562
+ .dbgOverlay() Toggle the pinned live overlay (queues, hints, timer)
13400
13563
  .showQueue() Show current queue state (active session only)
13401
13564
  .showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
13402
13565
  .showInterleaving(index?) Analyze course interleaving pattern
@@ -13465,6 +13628,13 @@ var SessionController = class _SessionController extends Loggable {
13465
13628
  * Used by nextCard() to await completion before drawing from queues.
13466
13629
  */
13467
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;
13468
13638
  /**
13469
13639
  * Number of well-indicated new cards remaining before the queue
13470
13640
  * degrades to poorly-indicated content. Decremented on each newQ
@@ -13577,6 +13747,7 @@ var SessionController = class _SessionController extends Loggable {
13577
13747
  endTime: ${this.endTime}
13578
13748
  defaultBatchLimit: ${this._defaultBatchLimit}
13579
13749
  initialReviewCap: ${this._initialReviewCap}`);
13750
+ registerActiveController(this);
13580
13751
  }
13581
13752
  tick() {
13582
13753
  this._secondsRemaining = Math.floor((this.endTime.valueOf() - Date.now()) / 1e3);
@@ -13677,15 +13848,23 @@ var SessionController = class _SessionController extends Loggable {
13677
13848
  );
13678
13849
  const inflight = this._replanPromise;
13679
13850
  const queued = inflight.catch(() => void 0).then(() => this._runReplan(opts));
13680
- this._replanPromise = queued.finally(() => {
13681
- if (this._replanPromise === queued) this._replanPromise = null;
13851
+ const tracked2 = queued.finally(() => {
13852
+ if (this._replanPromise === tracked2) {
13853
+ this._replanPromise = null;
13854
+ this._activeReplanLabel = null;
13855
+ }
13682
13856
  });
13857
+ this._replanPromise = tracked2;
13683
13858
  return queued;
13684
13859
  }
13685
13860
  const run = this._runReplan(opts);
13686
- this._replanPromise = run.finally(() => {
13687
- if (this._replanPromise === run) this._replanPromise = null;
13861
+ const tracked = run.finally(() => {
13862
+ if (this._replanPromise === tracked) {
13863
+ this._replanPromise = null;
13864
+ this._activeReplanLabel = null;
13865
+ }
13688
13866
  });
13867
+ this._replanPromise = tracked;
13689
13868
  await run;
13690
13869
  }
13691
13870
  /**
@@ -13694,7 +13873,6 @@ var SessionController = class _SessionController extends Loggable {
13694
13873
  * triggers in nextCard) return false and may coalesce.
13695
13874
  */
13696
13875
  _replanHasIntent(opts) {
13697
- if (opts.label) return true;
13698
13876
  if (opts.limit !== void 0) return true;
13699
13877
  if (opts.minFollowUpCards !== void 0) return true;
13700
13878
  if (opts.mode && opts.mode !== "replace") return true;
@@ -13715,6 +13893,7 @@ var SessionController = class _SessionController extends Loggable {
13715
13893
  * newQ.peek(0) is the imminent draw we need to exclude.
13716
13894
  */
13717
13895
  async _runReplan(opts) {
13896
+ this._activeReplanLabel = opts.label ?? "(auto)";
13718
13897
  if (!opts.hints) opts.hints = {};
13719
13898
  const hints = opts.hints;
13720
13899
  const excludeSet = new Set(hints.excludeCards ?? []);
@@ -13772,6 +13951,34 @@ var SessionController = class _SessionController extends Loggable {
13772
13951
  getSessionHints() {
13773
13952
  return this._sessionHints;
13774
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
+ }
13775
13982
  /**
13776
13983
  * Merge `hints` into the durable session hints via the pipeline's
13777
13984
  * `mergeHints` (boosts multiply, require/exclude lists concat-dedup).
@@ -13853,9 +14060,13 @@ var SessionController = class _SessionController extends Loggable {
13853
14060
  */
13854
14061
  async _replanUncoalesced(opts) {
13855
14062
  const run = this._runReplan(opts);
13856
- this._replanPromise = run.finally(() => {
13857
- if (this._replanPromise === run) this._replanPromise = null;
14063
+ const tracked = run.finally(() => {
14064
+ if (this._replanPromise === tracked) {
14065
+ this._replanPromise = null;
14066
+ this._activeReplanLabel = null;
14067
+ }
13858
14068
  });
14069
+ this._replanPromise = tracked;
13859
14070
  await run;
13860
14071
  }
13861
14072
  /**
@@ -14211,14 +14422,14 @@ var SessionController = class _SessionController extends Loggable {
14211
14422
  this.log(
14212
14423
  `[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
14213
14424
  );
14214
- void this.requestReplan();
14425
+ void this.requestReplan({ label: "auto:depletion" });
14215
14426
  }
14216
14427
  const REPLAN_BUFFER = 3;
14217
14428
  if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
14218
14429
  this.log(
14219
14430
  `[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (newQ: ${this.newQ.length}). Triggering background replan.`
14220
14431
  );
14221
- void this.requestReplan();
14432
+ void this.requestReplan({ label: "auto:quality" });
14222
14433
  }
14223
14434
  if (this._secondsRemaining <= 0 && this.failedQ.length === 0 && this._minCardsGuarantee <= 0) {
14224
14435
  this._currentCard = null;