@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.
Files changed (41) hide show
  1. package/dist/{contentSource-Ht3N2f-y.d.ts → contentSource-Cplhv3bJ.d.ts} +1 -1
  2. package/dist/{contentSource-BMlMwSiG.d.cts → contentSource-kI9_jwTu.d.cts} +1 -1
  3. package/dist/core/index.d.cts +5 -5
  4. package/dist/core/index.d.ts +5 -5
  5. package/dist/core/index.js +2 -1
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +2 -1
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BEqB8VBR.d.cts → dataLayerProvider-CiA2Rr0v.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-DObSXjnf.d.ts → dataLayerProvider-DrBqOUa3.d.ts} +1 -1
  11. package/dist/impl/couch/index.d.cts +3 -3
  12. package/dist/impl/couch/index.d.ts +3 -3
  13. package/dist/impl/couch/index.js +2 -1
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +2 -1
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.cts +4 -4
  18. package/dist/impl/static/index.d.ts +4 -4
  19. package/dist/impl/static/index.js +2 -1
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +2 -1
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/{index-BWvO-_rJ.d.ts → index-BLLT5BYE.d.ts} +1 -1
  24. package/dist/{index-Ba7hYbHj.d.cts → index-k9NFHpS1.d.cts} +1 -1
  25. package/dist/index.d.cts +209 -10
  26. package/dist/index.d.ts +209 -10
  27. package/dist/index.js +361 -17
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +361 -17
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/{types-W8n-B6HG.d.cts → types-BFUa1pa3.d.cts} +1 -1
  32. package/dist/{types-CJrLM1Ew.d.ts → types-CHgpWQAY.d.ts} +1 -1
  33. package/dist/{types-legacy-JXDxinpU.d.cts → types-legacy-4tlwHnXo.d.cts} +1 -1
  34. package/dist/{types-legacy-JXDxinpU.d.ts → types-legacy-4tlwHnXo.d.ts} +1 -1
  35. package/dist/util/packer/index.d.cts +3 -3
  36. package/dist/util/packer/index.d.ts +3 -3
  37. package/package.json +3 -3
  38. package/src/core/navigators/Pipeline.ts +1 -1
  39. package/src/study/SessionController.ts +347 -22
  40. package/src/study/SessionDebugger.ts +10 -0
  41. package/src/study/SessionOverlay.ts +276 -0
@@ -0,0 +1,276 @@
1
+ import { logger } from '../util/logger';
2
+ import type { ReplanHints } from '@db/core/navigators/generators/types';
3
+
4
+ // ============================================================================
5
+ // SESSION OVERLAY
6
+ // ============================================================================
7
+ //
8
+ // A pinned, vanilla-DOM debug overlay for the LIVE SessionController. Unlike
9
+ // `SessionDebugger` (a passive tracker of pushed snapshots), this reads the
10
+ // active controller directly each tick — current queues, session hints, timer.
11
+ //
12
+ // Toggled via `window.skuilder.session.dbgOverlay()`.
13
+ //
14
+ // The `db` package is framework-agnostic, so this renders with raw DOM and
15
+ // no-ops gracefully in non-browser hosts (e.g. the tuilder TUI). It is the
16
+ // first DOM-rendering debug util in the package — kept self-contained here.
17
+ //
18
+ // ============================================================================
19
+
20
+ /** Per-queue debug view: total length, cumulative draws, and head-first cardIDs. */
21
+ export interface SessionQueueDebug {
22
+ length: number;
23
+ dequeueCount: number;
24
+ /** cardIDs in queue order, head (next draw) first. */
25
+ cards: string[];
26
+ }
27
+
28
+ /** Live snapshot of the controller, read fresh on each overlay tick. */
29
+ export interface SessionDebugSnapshot {
30
+ secondsRemaining: number;
31
+ hasCardGuarantee: boolean;
32
+ minCardsGuarantee: number;
33
+ wellIndicatedRemaining: number;
34
+ /** cardID of the card currently in front of the learner, if any. */
35
+ currentCard: string | null;
36
+ /** Session-durable hints re-merged into every pipeline run this session. */
37
+ sessionHints: ReplanHints | null;
38
+ /** True while a replan is executing (in-flight). */
39
+ replanActive: boolean;
40
+ /** Reason for the in-flight replan (caller label, or '(auto)'); may be stale when idle. */
41
+ replanLabel: string | null;
42
+ reviewQ: SessionQueueDebug;
43
+ newQ: SessionQueueDebug;
44
+ failedQ: SessionQueueDebug;
45
+ }
46
+
47
+ /** The narrow surface the overlay needs from a SessionController. */
48
+ export interface SessionDebugTarget {
49
+ getDebugSnapshot(): SessionDebugSnapshot;
50
+ }
51
+
52
+ // ----------------------------------------------------------------------------
53
+ // Active-controller registry
54
+ // ----------------------------------------------------------------------------
55
+ //
56
+ // The controller registers itself on construction; a new session overwrites the
57
+ // prior handle. Kept here (a leaf module) so SessionController can import the
58
+ // registrar without pulling in the overlay's DOM code or risking an import
59
+ // cycle with SessionDebugger.
60
+
61
+ let activeController: SessionDebugTarget | null = null;
62
+
63
+ /** Called by SessionController's constructor. Pass `null` to deregister. */
64
+ export function registerActiveController(controller: SessionDebugTarget | null): void {
65
+ activeController = controller;
66
+ }
67
+
68
+ export function getActiveController(): SessionDebugTarget | null {
69
+ return activeController;
70
+ }
71
+
72
+ // ----------------------------------------------------------------------------
73
+ // Overlay rendering (vanilla DOM)
74
+ // ----------------------------------------------------------------------------
75
+
76
+ const OVERLAY_ID = 'skuilder-session-overlay';
77
+ const POLL_MS = 300;
78
+ /** Queues with at most this many cards are listed outright; larger ones collapse to a count. */
79
+ const INLINE_THRESHOLD = 5;
80
+
81
+ /** Braille spinner frames, advanced once per render tick (≈POLL_MS cadence). */
82
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
83
+ let spinnerFrame = 0;
84
+
85
+ let overlayEl: HTMLElement | null = null;
86
+ let pollHandle: ReturnType<typeof setInterval> | null = null;
87
+
88
+ /** Expansion state for collapsible (large) queues, preserved across re-renders. */
89
+ const expanded: Record<string, boolean> = { reviewQ: false, newQ: false, failedQ: false };
90
+
91
+ /**
92
+ * Toggle the pinned overlay on/off. No-ops (with a console hint) when there is
93
+ * no DOM, so it is safe to call from any host environment.
94
+ */
95
+ export function toggleSessionOverlay(): void {
96
+ if (typeof document === 'undefined') {
97
+ logger.info('[Session Overlay] No DOM available (non-browser host); overlay unavailable.');
98
+ return;
99
+ }
100
+ if (overlayEl) {
101
+ teardown();
102
+ logger.info('[Session Overlay] Hidden.');
103
+ } else {
104
+ mount();
105
+ logger.info('[Session Overlay] Shown. Toggle off with window.skuilder.session.dbgOverlay().');
106
+ }
107
+ }
108
+
109
+ function mount(): void {
110
+ overlayEl = document.createElement('div');
111
+ overlayEl.id = OVERLAY_ID;
112
+ Object.assign(overlayEl.style, {
113
+ position: 'fixed',
114
+ top: '8px',
115
+ left: '8px',
116
+ zIndex: '2147483647',
117
+ maxWidth: '320px',
118
+ maxHeight: '90vh',
119
+ overflowY: 'auto',
120
+ padding: '8px 10px',
121
+ background: 'rgba(17, 24, 39, 0.92)',
122
+ color: '#e5e7eb',
123
+ font: '11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace',
124
+ borderRadius: '6px',
125
+ boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
126
+ pointerEvents: 'auto',
127
+ userSelect: 'none',
128
+ });
129
+ document.body.appendChild(overlayEl);
130
+ render();
131
+ pollHandle = setInterval(render, POLL_MS);
132
+ }
133
+
134
+ function teardown(): void {
135
+ if (pollHandle !== null) {
136
+ clearInterval(pollHandle);
137
+ pollHandle = null;
138
+ }
139
+ if (overlayEl?.parentNode) {
140
+ overlayEl.parentNode.removeChild(overlayEl);
141
+ }
142
+ overlayEl = null;
143
+ }
144
+
145
+ function render(): void {
146
+ if (!overlayEl) return;
147
+
148
+ spinnerFrame++;
149
+
150
+ const ctrl = getActiveController();
151
+ if (!ctrl) {
152
+ overlayEl.innerHTML = headerHtml() + `<div style="opacity:.65">No active session.</div>`;
153
+ return;
154
+ }
155
+
156
+ const s = ctrl.getDebugSnapshot();
157
+ overlayEl.innerHTML =
158
+ headerHtml() +
159
+ replanHtml(s) +
160
+ metaHtml(s) +
161
+ hintsHtml(s.sessionHints) +
162
+ queueHtml('reviewQ', 'reviewQ', s.reviewQ) +
163
+ queueHtml('newQ', 'newQ', s.newQ) +
164
+ queueHtml('failedQ', 'failedQ', s.failedQ);
165
+
166
+ // Re-attach toggle handlers for collapsible queue headers each render.
167
+ overlayEl.querySelectorAll<HTMLElement>('[data-q]').forEach((el) => {
168
+ el.onclick = () => {
169
+ const key = el.dataset.q;
170
+ if (!key) return;
171
+ expanded[key] = !expanded[key];
172
+ render();
173
+ };
174
+ });
175
+ }
176
+
177
+ function headerHtml(): string {
178
+ return `<div style="font-weight:600;color:#93c5fd;margin-bottom:4px">⚙ SessionController</div>`;
179
+ }
180
+
181
+ function replanHtml(s: SessionDebugSnapshot): string {
182
+ if (!s.replanActive) {
183
+ return `<div style="margin-bottom:6px;opacity:.45">○ idle</div>`;
184
+ }
185
+ const frame = SPINNER_FRAMES[spinnerFrame % SPINNER_FRAMES.length];
186
+ const reason = esc(s.replanLabel ?? '(auto)');
187
+ return (
188
+ `<div style="margin-bottom:6px;color:#fde047">` +
189
+ `${frame} replanning <span style="opacity:.85">[${reason}]</span></div>`
190
+ );
191
+ }
192
+
193
+ function metaHtml(s: SessionDebugSnapshot): string {
194
+ const mmss = formatTime(s.secondsRemaining);
195
+ const guarantee = s.hasCardGuarantee
196
+ ? ` · <span style="color:#fbbf24">guarantee ${s.minCardsGuarantee}</span>`
197
+ : '';
198
+ const rows = [
199
+ `time ${mmss}${guarantee}`,
200
+ `well-indicated left: ${s.wellIndicatedRemaining}`,
201
+ `current: ${s.currentCard ? esc(s.currentCard) : '<span style="opacity:.6">—</span>'}`,
202
+ ];
203
+ return `<div style="margin-bottom:6px">${rows.map((r) => `<div>${r}</div>`).join('')}</div>`;
204
+ }
205
+
206
+ function hintsHtml(h: ReplanHints | null): string {
207
+ const parts: string[] = [];
208
+ if (h) {
209
+ if (h.boostTags && Object.keys(h.boostTags).length) {
210
+ parts.push(
211
+ `boost: ` +
212
+ Object.entries(h.boostTags)
213
+ .map(([k, v]) => `${esc(k)}<span style="opacity:.6">×${v}</span>`)
214
+ .join(', ')
215
+ );
216
+ }
217
+ if (h.boostCards && Object.keys(h.boostCards).length) {
218
+ parts.push(
219
+ `boostCards: ` +
220
+ Object.entries(h.boostCards)
221
+ .map(([k, v]) => `${esc(k)}<span style="opacity:.6">×${v}</span>`)
222
+ .join(', ')
223
+ );
224
+ }
225
+ if (h.requireCards?.length) parts.push(`require: ${h.requireCards.map(esc).join(', ')}`);
226
+ if (h.requireTags?.length) parts.push(`requireTags: ${h.requireTags.map(esc).join(', ')}`);
227
+ if (h.excludeTags?.length) parts.push(`exclude: ${h.excludeTags.map(esc).join(', ')}`);
228
+ if (h.excludeCards?.length) parts.push(`excludeCards: ${h.excludeCards.map(esc).join(', ')}`);
229
+ }
230
+ const body = parts.length
231
+ ? parts.map((p) => `<div style="margin-left:6px">${p}</div>`).join('')
232
+ : `<div style="margin-left:6px;opacity:.6">none</div>`;
233
+ return (
234
+ `<div style="margin-bottom:6px">` +
235
+ `<div style="color:#86efac">sessionHints</div>${body}</div>`
236
+ );
237
+ }
238
+
239
+ function queueHtml(key: string, label: string, q: SessionQueueDebug): string {
240
+ const collapsible = q.length > INLINE_THRESHOLD;
241
+ const isOpen = !collapsible || expanded[key];
242
+ const caret = collapsible ? (expanded[key] ? '▾ ' : '▸ ') : '';
243
+ const drawn = q.dequeueCount ? ` <span style="opacity:.5">drawn ${q.dequeueCount}</span>` : '';
244
+ const titleStyle = collapsible
245
+ ? 'cursor:pointer;color:#f9a8d4'
246
+ : 'color:#f9a8d4';
247
+ const titleAttr = collapsible ? ` data-q="${key}"` : '';
248
+ const title = `<div${titleAttr} style="${titleStyle}">${caret}${label}: ${q.length}${drawn}</div>`;
249
+
250
+ let body = '';
251
+ if (isOpen && q.cards.length) {
252
+ body =
253
+ `<ol style="margin:2px 0 6px 0;padding-left:20px">` +
254
+ q.cards.map((c) => `<li style="white-space:nowrap">${esc(c)}</li>`).join('') +
255
+ `</ol>`;
256
+ } else if (!q.cards.length) {
257
+ body = `<div style="margin:1px 0 6px 6px;opacity:.5">empty</div>`;
258
+ } else {
259
+ body = `<div style="margin:1px 0 6px 6px;opacity:.55">(${q.length} cards — click to expand)</div>`;
260
+ }
261
+ return title + body;
262
+ }
263
+
264
+ function formatTime(totalSeconds: number): string {
265
+ const s = Math.max(0, Math.round(totalSeconds));
266
+ const m = Math.floor(s / 60);
267
+ const r = s % 60;
268
+ return `${m}:${r.toString().padStart(2, '0')}`;
269
+ }
270
+
271
+ function esc(value: string): string {
272
+ return value
273
+ .replace(/&/g, '&amp;')
274
+ .replace(/</g, '&lt;')
275
+ .replace(/>/g, '&gt;');
276
+ }