@vue-skuilder/db 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/index.d.cts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +27 -17
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +27 -17
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +35 -17
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +35 -17
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +27 -17
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +27 -17
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +47 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +46 -25
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/core/navigators/SrsDebugger.ts +8 -7
- package/src/core/navigators/generators/srs.ts +40 -28
- package/src/impl/common/BaseUserDB.ts +13 -0
- package/src/impl/couch/CouchDBSyncStrategy.ts +17 -0
- package/src/study/SessionController.ts +5 -1
- package/src/study/SessionDebugger.ts +16 -8
- package/src/study/SessionOverlay.ts +17 -23
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.2.
|
|
7
|
+
"version": "0.2.13",
|
|
8
8
|
"description": "Database layer for vue-skuilder",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"module": "dist/index.mjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@nilock2/pouchdb-authentication": "^1.0.2",
|
|
51
|
-
"@vue-skuilder/common": "0.2.
|
|
51
|
+
"@vue-skuilder/common": "0.2.13",
|
|
52
52
|
"cross-fetch": "^4.1.0",
|
|
53
53
|
"moment": "^2.29.4",
|
|
54
54
|
"pouchdb": "^9.0.0",
|
|
@@ -62,5 +62,5 @@
|
|
|
62
62
|
"vite": "^8.0.0",
|
|
63
63
|
"vitest": "^4.1.0"
|
|
64
64
|
},
|
|
65
|
-
"stableVersion": "0.2.
|
|
65
|
+
"stableVersion": "0.2.13"
|
|
66
66
|
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
// ============================================================================
|
|
4
4
|
//
|
|
5
5
|
// A tiny module-level capture of the SRS generator's per-run backlog state,
|
|
6
|
-
// so the live session overlay can show
|
|
7
|
-
// (boosted) new cards
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// so the live session overlay can show how much runway remains before reviews
|
|
7
|
+
// out-compete the current (boosted) new/prescribed cards: the backlog
|
|
8
|
+
// multiplier is exponential and uncapped, so it always has headroom to climb
|
|
9
|
+
// further — the question is just how large the backlog needs to get before it
|
|
10
|
+
// crosses whatever the competing boosts currently sit at.
|
|
10
11
|
//
|
|
11
12
|
// Mirrors MixerDebugger/PipelineDebugger: the generator pushes a snapshot each
|
|
12
13
|
// run (keyed by course, latest wins); the overlay reads it via the controller's
|
|
@@ -23,10 +24,10 @@ export interface SrsBacklogDebug {
|
|
|
23
24
|
dueNow: number;
|
|
24
25
|
/** Healthy backlog threshold; multiplier is ×1.0 at or below this. */
|
|
25
26
|
healthyBacklog: number;
|
|
26
|
-
/** Global multiplier applied to every due review's urgency this run (1.0
|
|
27
|
+
/** Global multiplier applied to every due review's urgency this run (>= 1.0, unbounded). */
|
|
27
28
|
backlogMultiplier: number;
|
|
28
|
-
/**
|
|
29
|
-
|
|
29
|
+
/** Exponential growth-rate base the multiplier climbs by per multiple of healthyBacklog excess. */
|
|
30
|
+
backlogGrowthRate: number;
|
|
30
31
|
/** Highest review score produced this run (post-multiplier; can exceed 1.0); null if none due. */
|
|
31
32
|
topReviewScore: number | null;
|
|
32
33
|
/** Human-readable time until the next review comes due, or null if some are due now. */
|
|
@@ -42,19 +42,27 @@ import { logger } from '@db/util/logger';
|
|
|
42
42
|
const DEFAULT_HEALTHY_BACKLOG = 20;
|
|
43
43
|
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
45
|
+
* Growth-rate base for backlog pressure as a *multiplier* on review urgency.
|
|
46
46
|
*
|
|
47
|
-
* Backlog pressure is multiplicative (×1.0 at/below healthy
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
47
|
+
* Backlog pressure is multiplicative (×1.0 at/below healthy) and exponential
|
|
48
|
+
* in the backlog's excess over healthy, expressed in multiples of
|
|
49
|
+
* `healthyBacklog` (see computeBacklogMultiplier) — deliberately uncapped. It
|
|
50
|
+
* replaces an older additive +0..+0.5 term that was a [0,1]-era modifier —
|
|
51
|
+
* once review scores stopped being clamped to 1.0 and new cards could be
|
|
52
|
+
* boosted well past it (e.g. an intro ×5 → 7+), a flat +0.5 was both too small
|
|
53
|
+
* to compete and mostly eaten by the old 1.0 clamp. A linear-then-capped
|
|
54
|
+
* multiplier came next, but that just moved the problem: other generators
|
|
55
|
+
* (e.g. Prescribed Intro Backpressure) score on a much larger open scale with
|
|
56
|
+
* their own, independently-tuned caps, so a hard review-side ceiling meant
|
|
57
|
+
* reviews could never win out no matter how backlogged they got. Exponential
|
|
58
|
+
* growth has no such ceiling — a sufficiently neglected backlog keeps
|
|
59
|
+
* climbing until it outcompetes anything, which is the intended long-term
|
|
60
|
+
* fallback: other generators can dominate short-term, but SRS is the
|
|
61
|
+
* framework-invariant backstop and should always win eventually. Tunable —
|
|
62
|
+
* verify review vs new ordering in the dbg overlay's "review backpressure"
|
|
63
|
+
* panel.
|
|
56
64
|
*/
|
|
57
|
-
const
|
|
65
|
+
const BACKLOG_GROWTH_RATE = 2;
|
|
58
66
|
|
|
59
67
|
/**
|
|
60
68
|
* Configuration for the SRS strategy.
|
|
@@ -249,7 +257,7 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
|
|
|
249
257
|
dueNow: dueReviews.length,
|
|
250
258
|
healthyBacklog: this.healthyBacklog,
|
|
251
259
|
backlogMultiplier,
|
|
252
|
-
|
|
260
|
+
backlogGrowthRate: BACKLOG_GROWTH_RATE,
|
|
253
261
|
topReviewScore: sorted.length > 0 ? sorted[0].score : null,
|
|
254
262
|
nextDueIn,
|
|
255
263
|
timestamp: Date.now(),
|
|
@@ -268,28 +276,32 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
|
|
|
268
276
|
/**
|
|
269
277
|
* Compute the multiplicative backlog pressure based on number of due reviews.
|
|
270
278
|
*
|
|
271
|
-
* ×1.0 at or below the healthy threshold (no boost),
|
|
272
|
-
*
|
|
279
|
+
* ×1.0 at or below the healthy threshold (no boost); above it, grows as
|
|
280
|
+
* BACKLOG_GROWTH_RATE raised to the excess expressed in multiples of the
|
|
281
|
+
* healthy threshold. Uncapped — self-regulating instead: reviews winning
|
|
282
|
+
* slots depletes dueCount, which drops the ratio and relaxes the multiplier
|
|
283
|
+
* next run.
|
|
273
284
|
*
|
|
274
|
-
* Examples (with default healthyBacklog=20,
|
|
275
|
-
* - 10 due reviews → ×1.00
|
|
276
|
-
* - 20 due reviews → ×1.00
|
|
277
|
-
* - 40 due reviews → ×1.50
|
|
278
|
-
* - 60 due reviews → ×2.
|
|
285
|
+
* Examples (with default healthyBacklog=20, BACKLOG_GROWTH_RATE=1.5):
|
|
286
|
+
* - 10 due reviews → ×1.00 (healthy)
|
|
287
|
+
* - 20 due reviews → ×1.00 (at threshold, ratio 0)
|
|
288
|
+
* - 40 due reviews → ×1.50 (ratio 1, 2x threshold)
|
|
289
|
+
* - 60 due reviews → ×2.25 (ratio 2, 3x threshold — old hard cap was ×2.00 here)
|
|
290
|
+
* - 100 due reviews → ×5.06 (ratio 4, 5x threshold)
|
|
291
|
+
* - 160 due reviews → ×17.09 (ratio 7, 8x threshold)
|
|
279
292
|
*
|
|
280
293
|
* @param dueCount - Number of reviews currently due
|
|
281
|
-
* @returns Multiplier applied to review urgency (1.0
|
|
294
|
+
* @returns Multiplier applied to review urgency (>= 1.0, unbounded)
|
|
282
295
|
*/
|
|
283
296
|
private computeBacklogMultiplier(dueCount: number): number {
|
|
284
297
|
if (dueCount <= this.healthyBacklog) {
|
|
285
298
|
return 1.0;
|
|
286
299
|
}
|
|
287
300
|
|
|
288
|
-
// Linear in excess: ×1 at healthy, reaching MAX at 3× healthy (excess = 2×healthy).
|
|
289
301
|
const excess = dueCount - this.healthyBacklog;
|
|
290
|
-
const
|
|
302
|
+
const ratio = excess / this.healthyBacklog;
|
|
291
303
|
|
|
292
|
-
return Math.
|
|
304
|
+
return Math.pow(BACKLOG_GROWTH_RATE, ratio);
|
|
293
305
|
}
|
|
294
306
|
|
|
295
307
|
/**
|
|
@@ -306,17 +318,17 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
|
|
|
306
318
|
* - 180 days → ~0.30
|
|
307
319
|
*
|
|
308
320
|
* 3. Backlog pressure = global *multiplier* when review backlog exceeds the
|
|
309
|
-
* healthy threshold (×1.0 healthy →
|
|
321
|
+
* healthy threshold (×1.0 healthy → exponential growth, uncapped, above it).
|
|
310
322
|
*
|
|
311
323
|
* Combined: (base 0.5 + urgency factors * 0.45) × backlog multiplier.
|
|
312
324
|
* Per-card range before pressure: ~0.57–0.95. NOT clamped to 1.0 — under a
|
|
313
325
|
* heavy backlog reviews scale onto the open scale to compete with (and exceed)
|
|
314
|
-
* new cards;
|
|
315
|
-
*
|
|
326
|
+
* new cards; there's no ceiling at all now, so a bad enough backlog always
|
|
327
|
+
* wins eventually — self-regulating because winning slots depletes dueCount.
|
|
316
328
|
*
|
|
317
329
|
* @param review - The scheduled card to score
|
|
318
330
|
* @param now - Current time
|
|
319
|
-
* @param backlogMultiplier - Pre-computed backlog multiplier (1.0
|
|
331
|
+
* @param backlogMultiplier - Pre-computed backlog multiplier (>= 1.0, unbounded)
|
|
320
332
|
*/
|
|
321
333
|
private computeUrgencyScore(
|
|
322
334
|
review: ScheduledCard,
|
|
@@ -366,4 +378,4 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
|
|
|
366
378
|
|
|
367
379
|
return { score, reason };
|
|
368
380
|
}
|
|
369
|
-
}
|
|
381
|
+
}
|
|
@@ -214,6 +214,19 @@ Currently logged-in as ${this._username}.`
|
|
|
214
214
|
}
|
|
215
215
|
|
|
216
216
|
const ret = await this.syncStrategy.logout!();
|
|
217
|
+
|
|
218
|
+
// Mint a FRESH guest identity on logout rather than re-adopting whatever
|
|
219
|
+
// sk-guest-uuid happens to survive in localStorage. A stale UUID can point
|
|
220
|
+
// at an already-consumed guest DB (one previously migrated into a real
|
|
221
|
+
// account); re-adopting it would resurface that account's progress and, on
|
|
222
|
+
// the next signup, migrate it into the new account. Clearing the pointer
|
|
223
|
+
// forces accomodateGuest() to generate a new UUID + empty guest DB.
|
|
224
|
+
try {
|
|
225
|
+
localStorage.removeItem('sk-guest-uuid');
|
|
226
|
+
} catch (e) {
|
|
227
|
+
logger.warn('localStorage not available (Node.js environment):', e);
|
|
228
|
+
}
|
|
229
|
+
|
|
217
230
|
// return to 'guest' mode
|
|
218
231
|
this._username = await this.syncStrategy.getCurrentUsername();
|
|
219
232
|
await this.init();
|
|
@@ -228,6 +228,23 @@ export class CouchDBSyncStrategy implements SyncStrategy {
|
|
|
228
228
|
logger.info('No documents to migrate from funnel account');
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
// Destroy the consumed guest DB so it can never be re-migrated into a
|
|
232
|
+
// future account. Without this, an orphaned userdb-sk-guest-<uuid> lingers
|
|
233
|
+
// in IndexedDB indefinitely; if its stale identity is later re-adopted
|
|
234
|
+
// (e.g. on logout via a surviving sk-guest-uuid), its progress bleeds into
|
|
235
|
+
// the next new account. Best-effort: migration has already succeeded, so a
|
|
236
|
+
// teardown failure must not fail account creation.
|
|
237
|
+
try {
|
|
238
|
+
await oldLocalDB.destroy();
|
|
239
|
+
logger.info(`Destroyed consumed guest DB for ${oldUsername}`);
|
|
240
|
+
} catch (destroyErr) {
|
|
241
|
+
logger.warn(
|
|
242
|
+
`Failed to destroy consumed guest DB for ${oldUsername} (non-fatal): ${
|
|
243
|
+
destroyErr instanceof Error ? destroyErr.message : String(destroyErr)
|
|
244
|
+
}`
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
231
248
|
return { success: true };
|
|
232
249
|
} catch (error) {
|
|
233
250
|
logger.error('Migration failed:', error);
|
|
@@ -20,7 +20,7 @@ import { ReplanHints } from '@db/core/navigators/generators/types';
|
|
|
20
20
|
import { mergeHints } from '@db/core/navigators/Pipeline';
|
|
21
21
|
import { SourceMixer, QuotaRoundRobinMixer, SourceBatch } from './SourceMixer';
|
|
22
22
|
import { captureMixerRun } from './MixerDebugger';
|
|
23
|
-
import { startSessionTracking, recordCardPresentation, snapshotQueues, endSessionTracking } from './SessionDebugger';
|
|
23
|
+
import { startSessionTracking, recordCardPresentation, snapshotQueues, endSessionTracking, clearStaleSessionDebugState } from './SessionDebugger';
|
|
24
24
|
import {
|
|
25
25
|
registerActiveController,
|
|
26
26
|
type SessionDebugSnapshot,
|
|
@@ -475,6 +475,10 @@ export class SessionController<TView = unknown> extends Loggable {
|
|
|
475
475
|
);
|
|
476
476
|
}
|
|
477
477
|
|
|
478
|
+
// Clear the previous session's debug state before this session's
|
|
479
|
+
// bootstrap pipeline run fires, so that run's own report isn't wiped out.
|
|
480
|
+
clearStaleSessionDebugState();
|
|
481
|
+
|
|
478
482
|
const wellIndicated = await this.getWeightedContent();
|
|
479
483
|
this._wellIndicatedRemaining = wellIndicated;
|
|
480
484
|
if (wellIndicated >= 0 && wellIndicated < SessionController.MIN_WELL_INDICATED) {
|
|
@@ -68,6 +68,22 @@ let activeSession: SessionRunReport | null = null;
|
|
|
68
68
|
const sessionHistory: SessionRunReport[] = [];
|
|
69
69
|
const MAX_HISTORY = 5;
|
|
70
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Release pipeline run-history / SRS backlog debug memory from the previous
|
|
73
|
+
* session before this session begins piling new runs on top of it. Must be
|
|
74
|
+
* called before the first pipeline run of the new session (including the
|
|
75
|
+
* bootstrap run in prepareSession) — clearing on session START rather than
|
|
76
|
+
* END keeps post-session inspection, the dominant debugging workflow, fully
|
|
77
|
+
* functional: a finished session's run history sits intact until the user
|
|
78
|
+
* actually begins another one. It's a separate call from startSessionTracking
|
|
79
|
+
* because that function can't run until supplyQ/failedQ are populated by the
|
|
80
|
+
* bootstrap pipeline run, which would otherwise be cleared out from under it.
|
|
81
|
+
*/
|
|
82
|
+
export function clearStaleSessionDebugState(): void {
|
|
83
|
+
clearPipelineRunHistory();
|
|
84
|
+
clearSrsBacklogDebug();
|
|
85
|
+
}
|
|
86
|
+
|
|
71
87
|
/**
|
|
72
88
|
* Start tracking a new session.
|
|
73
89
|
*/
|
|
@@ -75,14 +91,6 @@ export function startSessionTracking(
|
|
|
75
91
|
supplyQLength: number,
|
|
76
92
|
failedQLength: number
|
|
77
93
|
): void {
|
|
78
|
-
// Release pipeline run-history memory before this session begins piling
|
|
79
|
-
// new runs on top of the previous session's retained reports. Clearing on
|
|
80
|
-
// session START (rather than END) keeps post-session inspection — the
|
|
81
|
-
// dominant debugging workflow — fully functional: a finished session's
|
|
82
|
-
// run history sits intact until the user actually begins another one.
|
|
83
|
-
clearPipelineRunHistory();
|
|
84
|
-
clearSrsBacklogDebug();
|
|
85
|
-
|
|
86
94
|
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
87
95
|
|
|
88
96
|
activeSession = {
|
|
@@ -365,25 +365,24 @@ function hintsHtml(h: ReplanHints | null): string {
|
|
|
365
365
|
|
|
366
366
|
/**
|
|
367
367
|
* SRS backlog panel — answers "is review starvation permanent?". Backlog
|
|
368
|
-
* pressure is a multiplier (×1.0 healthy →
|
|
369
|
-
* no longer clamped to 1.0, so under a heavy backlog they
|
|
370
|
-
* with (and exceed) new cards.
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
*
|
|
375
|
-
*
|
|
368
|
+
* pressure is a multiplier (×1.0 healthy → exponential, uncapped) on review
|
|
369
|
+
* urgency; reviews are no longer clamped to 1.0, so under a heavy backlog they
|
|
370
|
+
* scale up to compete with (and exceed) new cards. There's no ceiling, so
|
|
371
|
+
* reviews always win eventually — the panel shows how far the current
|
|
372
|
+
* multiplier is from that crossover, not whether it's "maxed": compare
|
|
373
|
+
* `top review` here against the supplyQ head score to see how much further
|
|
374
|
+
* the backlog needs to grow (or the boosts need to relax) before reviews win
|
|
375
|
+
* slots.
|
|
376
376
|
*/
|
|
377
377
|
function backlogHtml(backlog: SrsBacklogDebug[]): string {
|
|
378
378
|
if (!backlog.length) return '';
|
|
379
379
|
const rows = backlog
|
|
380
380
|
.map((b) => {
|
|
381
|
-
const
|
|
382
|
-
const multColor = b.backlogMultiplier <= 1 ? '#86efac' :
|
|
383
|
-
const headroom =
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
? 'climbing as backlog grows'
|
|
381
|
+
const hot = b.backlogMultiplier >= 3;
|
|
382
|
+
const multColor = b.backlogMultiplier <= 1 ? '#86efac' : hot ? '#fca5a5' : '#fcd34d';
|
|
383
|
+
const headroom =
|
|
384
|
+
b.backlogMultiplier > 1
|
|
385
|
+
? `climbing (×${b.backlogGrowthRate.toFixed(2)} per ${b.healthyBacklog}-review excess, unbounded)`
|
|
387
386
|
: 'healthy — no pressure';
|
|
388
387
|
const top = b.topReviewScore !== null ? b.topReviewScore.toFixed(2) : '—';
|
|
389
388
|
const next = b.nextDueIn ? ` <span style="opacity:.6">· next due ${esc(b.nextDueIn)}</span>` : '';
|
|
@@ -392,7 +391,7 @@ function backlogHtml(backlog: SrsBacklogDebug[]): string {
|
|
|
392
391
|
`<span style="opacity:.7">${esc(b.courseId.slice(0, 8))}</span> ` +
|
|
393
392
|
`due ${b.dueNow}/${b.scheduledTotal} <span style="opacity:.6">(healthy ${b.healthyBacklog})</span>${next}` +
|
|
394
393
|
`<div style="margin-left:6px">` +
|
|
395
|
-
`pressure <span style="color:${multColor}">×${b.backlogMultiplier.toFixed(2)}
|
|
394
|
+
`pressure <span style="color:${multColor}">×${b.backlogMultiplier.toFixed(2)}</span> ` +
|
|
396
395
|
`<span style="opacity:.6">${headroom} · top review ${top}</span></div>` +
|
|
397
396
|
`</div>`
|
|
398
397
|
);
|
|
@@ -541,18 +540,13 @@ function snapshotToText(s: SessionDebugSnapshot | null): string {
|
|
|
541
540
|
lines.push('');
|
|
542
541
|
lines.push('review backpressure:');
|
|
543
542
|
for (const b of s.reviewBacklog) {
|
|
544
|
-
const
|
|
545
|
-
const headroom = maxed
|
|
546
|
-
? 'maxed (boosts decide order)'
|
|
547
|
-
: b.backlogMultiplier > 1
|
|
548
|
-
? 'climbing'
|
|
549
|
-
: 'healthy';
|
|
543
|
+
const headroom = b.backlogMultiplier > 1 ? 'climbing' : 'healthy';
|
|
550
544
|
const top = b.topReviewScore !== null ? b.topReviewScore.toFixed(2) : '—';
|
|
551
545
|
const next = b.nextDueIn ? `, next due ${b.nextDueIn}` : '';
|
|
552
546
|
lines.push(
|
|
553
547
|
` ${b.courseId.slice(0, 8)}: due ${b.dueNow}/${b.scheduledTotal} ` +
|
|
554
|
-
`(healthy ${b.healthyBacklog})${next}; pressure ×${b.backlogMultiplier.toFixed(2)}
|
|
555
|
-
`${
|
|
548
|
+
`(healthy ${b.healthyBacklog})${next}; pressure ×${b.backlogMultiplier.toFixed(2)} ` +
|
|
549
|
+
`${headroom}; top review ${top}`
|
|
556
550
|
);
|
|
557
551
|
}
|
|
558
552
|
}
|