@vue-skuilder/db 0.2.7 → 0.2.9
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-Cplhv3bJ.d.ts → contentSource-C-0t0y0V.d.ts} +7 -0
- package/dist/{contentSource-kI9_jwTu.d.cts → contentSource-jSkcOt2s.d.cts} +7 -0
- package/dist/core/index.d.cts +67 -4
- package/dist/core/index.d.ts +67 -4
- package/dist/core/index.js +201 -39
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +198 -39
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-DrBqOUa3.d.ts → dataLayerProvider-BB0oi9T0.d.ts} +1 -1
- package/dist/{dataLayerProvider-CiA2Rr0v.d.cts → dataLayerProvider-BDClIrFC.d.cts} +1 -1
- package/dist/impl/couch/index.d.cts +2 -2
- package/dist/impl/couch/index.d.ts +2 -2
- package/dist/impl/couch/index.js +195 -39
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +195 -39
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +2 -2
- package/dist/impl/static/index.d.ts +2 -2
- package/dist/impl/static/index.js +195 -39
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +195 -39
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +115 -81
- package/dist/index.d.ts +115 -81
- package/dist/index.js +440 -251
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +437 -251
- package/dist/index.mjs.map +1 -1
- package/docs/navigators-architecture.md +29 -13
- package/package.json +3 -3
- package/src/core/interfaces/contentSource.ts +7 -0
- package/src/core/navigators/Pipeline.ts +93 -1
- package/src/core/navigators/PipelineDebugger.ts +11 -1
- package/src/core/navigators/SrsDebugger.ts +53 -0
- package/src/core/navigators/generators/prescribed.ts +76 -9
- package/src/core/navigators/generators/srs.ts +81 -37
- package/src/core/navigators/index.ts +9 -0
- package/src/study/SessionController.ts +260 -249
- package/src/study/SessionDebugger.ts +15 -25
- package/src/study/SessionOverlay.ts +108 -13
|
@@ -593,6 +593,7 @@ __export(PipelineDebugger_exports, {
|
|
|
593
593
|
buildRunReport: () => buildRunReport,
|
|
594
594
|
captureRun: () => captureRun,
|
|
595
595
|
clearRunHistory: () => clearRunHistory,
|
|
596
|
+
getActivePipeline: () => getActivePipeline,
|
|
596
597
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
597
598
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
598
599
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -600,6 +601,9 @@ __export(PipelineDebugger_exports, {
|
|
|
600
601
|
function registerPipelineForDebug(pipeline) {
|
|
601
602
|
_activePipeline = pipeline;
|
|
602
603
|
}
|
|
604
|
+
function getActivePipeline() {
|
|
605
|
+
return _activePipeline;
|
|
606
|
+
}
|
|
603
607
|
function clearRunHistory() {
|
|
604
608
|
runHistory.length = 0;
|
|
605
609
|
}
|
|
@@ -1415,6 +1419,30 @@ Example:
|
|
|
1415
1419
|
}
|
|
1416
1420
|
});
|
|
1417
1421
|
|
|
1422
|
+
// src/core/navigators/SrsDebugger.ts
|
|
1423
|
+
var SrsDebugger_exports = {};
|
|
1424
|
+
__export(SrsDebugger_exports, {
|
|
1425
|
+
captureSrsBacklog: () => captureSrsBacklog,
|
|
1426
|
+
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
1427
|
+
getSrsBacklogDebug: () => getSrsBacklogDebug
|
|
1428
|
+
});
|
|
1429
|
+
function captureSrsBacklog(snapshot) {
|
|
1430
|
+
snapshots.set(snapshot.courseId, snapshot);
|
|
1431
|
+
}
|
|
1432
|
+
function getSrsBacklogDebug() {
|
|
1433
|
+
return [...snapshots.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
1434
|
+
}
|
|
1435
|
+
function clearSrsBacklogDebug() {
|
|
1436
|
+
snapshots.clear();
|
|
1437
|
+
}
|
|
1438
|
+
var snapshots;
|
|
1439
|
+
var init_SrsDebugger = __esm({
|
|
1440
|
+
"src/core/navigators/SrsDebugger.ts"() {
|
|
1441
|
+
"use strict";
|
|
1442
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1418
1446
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
1419
1447
|
var CompositeGenerator_exports = {};
|
|
1420
1448
|
__export(CompositeGenerator_exports, {
|
|
@@ -1782,7 +1810,7 @@ function shuffleInPlace(arr) {
|
|
|
1782
1810
|
function pickTopByScore(cards, limit) {
|
|
1783
1811
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1784
1812
|
}
|
|
1785
|
-
var DEFAULT_FRESHNESS_WINDOW, DEFAULT_MAX_DIRECT_PER_RUN, DEFAULT_MAX_SUPPORT_PER_RUN, DEFAULT_HIERARCHY_DEPTH, DEFAULT_MIN_COUNT, DEFAULT_PRACTICE_MIN_COUNT, DEFAULT_MAX_PRACTICE_PER_RUN, BASE_TARGET_SCORE, BASE_SUPPORT_SCORE, DISCOVERED_SUPPORT_SCORE, BASE_PRACTICE_SCORE, MAX_TARGET_MULTIPLIER, MAX_SUPPORT_MULTIPLIER, PRESCRIBED_DEBUG_VERSION, PrescribedCardsGenerator;
|
|
1813
|
+
var DEFAULT_FRESHNESS_WINDOW, DEFAULT_MAX_DIRECT_PER_RUN, DEFAULT_MAX_SUPPORT_PER_RUN, DEFAULT_HIERARCHY_DEPTH, DEFAULT_MIN_COUNT, DEFAULT_PRACTICE_MIN_COUNT, DEFAULT_MAX_PRACTICE_PER_RUN, BASE_TARGET_SCORE, BASE_SUPPORT_SCORE, DISCOVERED_SUPPORT_SCORE, BASE_PRACTICE_SCORE, PRACTICE_BASE_MULT, MAX_PRACTICE_MULTIPLIER, PRACTICE_STALENESS_BUMP_PER_DAY, MAX_TARGET_MULTIPLIER, MAX_SUPPORT_MULTIPLIER, PRESCRIBED_DEBUG_VERSION, PrescribedCardsGenerator;
|
|
1786
1814
|
var init_prescribed = __esm({
|
|
1787
1815
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1788
1816
|
"use strict";
|
|
@@ -1799,6 +1827,9 @@ var init_prescribed = __esm({
|
|
|
1799
1827
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1800
1828
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
1801
1829
|
BASE_PRACTICE_SCORE = 1;
|
|
1830
|
+
PRACTICE_BASE_MULT = 2;
|
|
1831
|
+
MAX_PRACTICE_MULTIPLIER = 4;
|
|
1832
|
+
PRACTICE_STALENESS_BUMP_PER_DAY = 0.5;
|
|
1802
1833
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1803
1834
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1804
1835
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -1858,6 +1889,8 @@ var init_prescribed = __esm({
|
|
|
1858
1889
|
const emitted = [];
|
|
1859
1890
|
const emittedIds = /* @__PURE__ */ new Set();
|
|
1860
1891
|
const groupRuntimes = [];
|
|
1892
|
+
const priorPracticeDebt = progress.practiceDebt ?? {};
|
|
1893
|
+
const nextPracticeDebt = {};
|
|
1861
1894
|
for (const group of this.config.groups) {
|
|
1862
1895
|
const runtime = this.buildGroupRuntimeState({
|
|
1863
1896
|
group,
|
|
@@ -1915,10 +1948,13 @@ var init_prescribed = __esm({
|
|
|
1915
1948
|
userTagElo,
|
|
1916
1949
|
userGlobalElo,
|
|
1917
1950
|
activeIds,
|
|
1918
|
-
seenIds
|
|
1951
|
+
seenIds,
|
|
1952
|
+
priorPracticeDebt,
|
|
1953
|
+
nextPracticeDebt
|
|
1919
1954
|
});
|
|
1920
1955
|
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
1921
1956
|
}
|
|
1957
|
+
nextState.practiceDebt = nextPracticeDebt;
|
|
1922
1958
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
1923
1959
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
1924
1960
|
boostTags: hintSummary.boostTags,
|
|
@@ -2252,9 +2288,16 @@ var init_prescribed = __esm({
|
|
|
2252
2288
|
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2253
2289
|
* into the candidate pool. It exists because global-ELO retrieval
|
|
2254
2290
|
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2255
|
-
* freshly-introduced skill — putting them in the pool here
|
|
2256
|
-
*
|
|
2257
|
-
*
|
|
2291
|
+
* freshly-introduced skill — putting them in the pool here guarantees presence.
|
|
2292
|
+
*
|
|
2293
|
+
* Emphasis is a **practice-debt pressure** (parallel to SRS backlog pressure):
|
|
2294
|
+
* cards score `base × multiplier`, where the multiplier starts at
|
|
2295
|
+
* PRACTICE_BASE_MULT (so a few reps land promptly post-intro, competing with
|
|
2296
|
+
* pressured reviews) and escalates by how long the debt has stayed open
|
|
2297
|
+
* (per-tag, time-based via `priorPracticeDebt`/`nextPracticeDebt`), clamped at
|
|
2298
|
+
* MAX_PRACTICE_MULTIPLIER. The debt is durable and self-discharges the instant
|
|
2299
|
+
* the skill reaches `practiceMinCount` — so this no longer relies on the
|
|
2300
|
+
* session-scoped intro boost to actually surface.
|
|
2258
2301
|
*
|
|
2259
2302
|
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2260
2303
|
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
@@ -2269,7 +2312,9 @@ var init_prescribed = __esm({
|
|
|
2269
2312
|
userTagElo,
|
|
2270
2313
|
userGlobalElo,
|
|
2271
2314
|
activeIds,
|
|
2272
|
-
seenIds
|
|
2315
|
+
seenIds,
|
|
2316
|
+
priorPracticeDebt,
|
|
2317
|
+
nextPracticeDebt
|
|
2273
2318
|
} = args;
|
|
2274
2319
|
const patterns = group.practiceTagPatterns ?? [];
|
|
2275
2320
|
if (patterns.length === 0) return [];
|
|
@@ -2279,6 +2324,20 @@ var init_prescribed = __esm({
|
|
|
2279
2324
|
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2280
2325
|
);
|
|
2281
2326
|
if (practiceTags.length === 0) return [];
|
|
2327
|
+
const now = Date.now();
|
|
2328
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2329
|
+
const tagMultiplier = /* @__PURE__ */ new Map();
|
|
2330
|
+
for (const tag of practiceTags) {
|
|
2331
|
+
const firstOwedAt = priorPracticeDebt[tag] ?? isoNow();
|
|
2332
|
+
nextPracticeDebt[tag] = firstOwedAt;
|
|
2333
|
+
const staleDays = Math.max(0, (now - new Date(firstOwedAt).getTime()) / DAY_MS);
|
|
2334
|
+
const mult = clamp(
|
|
2335
|
+
PRACTICE_BASE_MULT + staleDays * PRACTICE_STALENESS_BUMP_PER_DAY,
|
|
2336
|
+
PRACTICE_BASE_MULT,
|
|
2337
|
+
MAX_PRACTICE_MULTIPLIER
|
|
2338
|
+
);
|
|
2339
|
+
tagMultiplier.set(tag, mult);
|
|
2340
|
+
}
|
|
2282
2341
|
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2283
2342
|
supportTags: practiceTags,
|
|
2284
2343
|
cardsByTag,
|
|
@@ -2294,18 +2353,25 @@ var init_prescribed = __esm({
|
|
|
2294
2353
|
const cards = [];
|
|
2295
2354
|
for (const cardId of practiceCardIds) {
|
|
2296
2355
|
emittedIds.add(cardId);
|
|
2356
|
+
let mult = PRACTICE_BASE_MULT;
|
|
2357
|
+
for (const tag of practiceTags) {
|
|
2358
|
+
if (cardsByTag.get(tag)?.includes(cardId) ?? false) {
|
|
2359
|
+
mult = Math.max(mult, tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT);
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
const score = BASE_PRACTICE_SCORE * mult;
|
|
2297
2363
|
cards.push({
|
|
2298
2364
|
cardId,
|
|
2299
2365
|
courseId,
|
|
2300
|
-
score
|
|
2366
|
+
score,
|
|
2301
2367
|
provenance: [
|
|
2302
2368
|
{
|
|
2303
2369
|
strategy: "prescribed",
|
|
2304
2370
|
strategyName: this.strategyName || this.name,
|
|
2305
2371
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2306
2372
|
action: "generated",
|
|
2307
|
-
score
|
|
2308
|
-
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2373
|
+
score,
|
|
2374
|
+
reason: `mode=practice;group=${group.id};debtMult=\xD7${mult.toFixed(2)};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2309
2375
|
}
|
|
2310
2376
|
]
|
|
2311
2377
|
});
|
|
@@ -2479,14 +2545,15 @@ __export(srs_exports, {
|
|
|
2479
2545
|
default: () => SRSNavigator
|
|
2480
2546
|
});
|
|
2481
2547
|
import moment3 from "moment";
|
|
2482
|
-
var DEFAULT_HEALTHY_BACKLOG,
|
|
2548
|
+
var DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_MULTIPLIER, SRSNavigator;
|
|
2483
2549
|
var init_srs = __esm({
|
|
2484
2550
|
"src/core/navigators/generators/srs.ts"() {
|
|
2485
2551
|
"use strict";
|
|
2486
2552
|
init_navigators();
|
|
2553
|
+
init_SrsDebugger();
|
|
2487
2554
|
init_logger();
|
|
2488
2555
|
DEFAULT_HEALTHY_BACKLOG = 20;
|
|
2489
|
-
|
|
2556
|
+
MAX_BACKLOG_MULTIPLIER = 2;
|
|
2490
2557
|
SRSNavigator = class extends ContentNavigator {
|
|
2491
2558
|
/** Human-readable name for CardGenerator interface */
|
|
2492
2559
|
name;
|
|
@@ -2553,9 +2620,18 @@ var init_srs = __esm({
|
|
|
2553
2620
|
}
|
|
2554
2621
|
}
|
|
2555
2622
|
}
|
|
2556
|
-
const
|
|
2623
|
+
const backlogMultiplier = this.computeBacklogMultiplier(dueReviews.length);
|
|
2624
|
+
const notDue = reviews.filter((r) => !now.isAfter(moment3.utc(r.reviewTime)));
|
|
2625
|
+
let nextDueIn = null;
|
|
2626
|
+
if (notDue.length > 0) {
|
|
2627
|
+
const next = notDue.reduce(
|
|
2628
|
+
(a, b) => moment3.utc(a.reviewTime).isBefore(moment3.utc(b.reviewTime)) ? a : b
|
|
2629
|
+
);
|
|
2630
|
+
const until = moment3.duration(moment3.utc(next.reviewTime).diff(now));
|
|
2631
|
+
nextDueIn = until.asHours() < 1 ? `${Math.round(until.asMinutes())}m` : until.asHours() < 24 ? `${Math.round(until.asHours())}h` : `${Math.round(until.asDays())}d`;
|
|
2632
|
+
}
|
|
2557
2633
|
if (dueReviews.length > 0) {
|
|
2558
|
-
const pressureNote =
|
|
2634
|
+
const pressureNote = backlogMultiplier > 1 ? ` [backlog pressure: \xD7${backlogMultiplier.toFixed(2)}]` : ` [healthy backlog]`;
|
|
2559
2635
|
logger.info(
|
|
2560
2636
|
`[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
|
|
2561
2637
|
);
|
|
@@ -2574,7 +2650,7 @@ var init_srs = __esm({
|
|
|
2574
2650
|
logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
|
|
2575
2651
|
}
|
|
2576
2652
|
const scored = dueReviews.map((review) => {
|
|
2577
|
-
const { score, reason } = this.computeUrgencyScore(review, now,
|
|
2653
|
+
const { score, reason } = this.computeUrgencyScore(review, now, backlogMultiplier);
|
|
2578
2654
|
return {
|
|
2579
2655
|
cardId: review.cardId,
|
|
2580
2656
|
courseId: review.courseId,
|
|
@@ -2592,30 +2668,42 @@ var init_srs = __esm({
|
|
|
2592
2668
|
]
|
|
2593
2669
|
};
|
|
2594
2670
|
});
|
|
2595
|
-
|
|
2671
|
+
const sorted = scored.sort((a, b) => b.score - a.score);
|
|
2672
|
+
captureSrsBacklog({
|
|
2673
|
+
courseId,
|
|
2674
|
+
scheduledTotal: reviews.length,
|
|
2675
|
+
dueNow: dueReviews.length,
|
|
2676
|
+
healthyBacklog: this.healthyBacklog,
|
|
2677
|
+
backlogMultiplier,
|
|
2678
|
+
maxBacklogMultiplier: MAX_BACKLOG_MULTIPLIER,
|
|
2679
|
+
topReviewScore: sorted.length > 0 ? sorted[0].score : null,
|
|
2680
|
+
nextDueIn,
|
|
2681
|
+
timestamp: Date.now()
|
|
2682
|
+
});
|
|
2683
|
+
return { cards: sorted.slice(0, limit) };
|
|
2596
2684
|
}
|
|
2597
2685
|
/**
|
|
2598
|
-
* Compute backlog pressure based on number of due reviews.
|
|
2686
|
+
* Compute the multiplicative backlog pressure based on number of due reviews.
|
|
2599
2687
|
*
|
|
2600
|
-
*
|
|
2601
|
-
* and
|
|
2688
|
+
* ×1.0 at or below the healthy threshold (no boost), increasing linearly above
|
|
2689
|
+
* it and maxing out at MAX_BACKLOG_MULTIPLIER at 3× the healthy backlog.
|
|
2602
2690
|
*
|
|
2603
|
-
* Examples (with default healthyBacklog=20):
|
|
2604
|
-
* - 10 due reviews →
|
|
2605
|
-
* - 20 due reviews →
|
|
2606
|
-
* - 40 due reviews →
|
|
2607
|
-
* - 60 due reviews →
|
|
2691
|
+
* Examples (with default healthyBacklog=20, MAX_BACKLOG_MULTIPLIER=2.0):
|
|
2692
|
+
* - 10 due reviews → ×1.00 (healthy)
|
|
2693
|
+
* - 20 due reviews → ×1.00 (at threshold)
|
|
2694
|
+
* - 40 due reviews → ×1.50 (2x threshold)
|
|
2695
|
+
* - 60 due reviews → ×2.00 (3x threshold, maxed)
|
|
2608
2696
|
*
|
|
2609
2697
|
* @param dueCount - Number of reviews currently due
|
|
2610
|
-
* @returns
|
|
2698
|
+
* @returns Multiplier applied to review urgency (1.0 to MAX_BACKLOG_MULTIPLIER)
|
|
2611
2699
|
*/
|
|
2612
|
-
|
|
2700
|
+
computeBacklogMultiplier(dueCount) {
|
|
2613
2701
|
if (dueCount <= this.healthyBacklog) {
|
|
2614
|
-
return
|
|
2702
|
+
return 1;
|
|
2615
2703
|
}
|
|
2616
2704
|
const excess = dueCount - this.healthyBacklog;
|
|
2617
|
-
const
|
|
2618
|
-
return Math.min(
|
|
2705
|
+
const multiplier = 1 + excess / this.healthyBacklog * ((MAX_BACKLOG_MULTIPLIER - 1) / 2);
|
|
2706
|
+
return Math.min(MAX_BACKLOG_MULTIPLIER, multiplier);
|
|
2619
2707
|
}
|
|
2620
2708
|
/**
|
|
2621
2709
|
* Compute urgency score for a review card.
|
|
@@ -2630,19 +2718,20 @@ var init_srs = __esm({
|
|
|
2630
2718
|
* - 30 days (720h) → ~0.56
|
|
2631
2719
|
* - 180 days → ~0.30
|
|
2632
2720
|
*
|
|
2633
|
-
* 3. Backlog pressure = global
|
|
2634
|
-
*
|
|
2635
|
-
* - At 2x healthy: +0.25
|
|
2636
|
-
* - At 3x+ healthy: +0.50 (max)
|
|
2721
|
+
* 3. Backlog pressure = global *multiplier* when review backlog exceeds the
|
|
2722
|
+
* healthy threshold (×1.0 healthy → up to MAX_BACKLOG_MULTIPLIER at 3×).
|
|
2637
2723
|
*
|
|
2638
|
-
* Combined: base 0.5 +
|
|
2639
|
-
*
|
|
2724
|
+
* Combined: (base 0.5 + urgency factors * 0.45) × backlog multiplier.
|
|
2725
|
+
* Per-card range before pressure: ~0.57–0.95. NOT clamped to 1.0 — under a
|
|
2726
|
+
* heavy backlog reviews scale onto the open scale to compete with (and exceed)
|
|
2727
|
+
* new cards; what keeps them from running away is the bounded multiplier, not
|
|
2728
|
+
* a hard ceiling.
|
|
2640
2729
|
*
|
|
2641
2730
|
* @param review - The scheduled card to score
|
|
2642
2731
|
* @param now - Current time
|
|
2643
|
-
* @param
|
|
2732
|
+
* @param backlogMultiplier - Pre-computed backlog multiplier (1.0 to MAX_BACKLOG_MULTIPLIER)
|
|
2644
2733
|
*/
|
|
2645
|
-
computeUrgencyScore(review, now,
|
|
2734
|
+
computeUrgencyScore(review, now, backlogMultiplier) {
|
|
2646
2735
|
const scheduledAt = moment3.utc(review.scheduledAt);
|
|
2647
2736
|
const due = moment3.utc(review.reviewTime);
|
|
2648
2737
|
const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
|
|
@@ -2652,15 +2741,15 @@ var init_srs = __esm({
|
|
|
2652
2741
|
const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
|
|
2653
2742
|
const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
|
|
2654
2743
|
const baseScore = 0.5 + urgency * 0.45;
|
|
2655
|
-
const score =
|
|
2744
|
+
const score = baseScore * backlogMultiplier;
|
|
2656
2745
|
const reasonParts = [
|
|
2657
2746
|
`${Math.round(hoursOverdue)}h overdue`,
|
|
2658
2747
|
`interval: ${Math.round(intervalHours)}h`,
|
|
2659
2748
|
`relative: ${relativeOverdue.toFixed(2)}`,
|
|
2660
2749
|
`recency: ${recencyFactor.toFixed(2)}`
|
|
2661
2750
|
];
|
|
2662
|
-
if (
|
|
2663
|
-
reasonParts.push(`backlog:
|
|
2751
|
+
if (backlogMultiplier > 1) {
|
|
2752
|
+
reasonParts.push(`backlog: \xD7${backlogMultiplier.toFixed(2)}`);
|
|
2664
2753
|
}
|
|
2665
2754
|
reasonParts.push("review");
|
|
2666
2755
|
const reason = reasonParts.join(", ");
|
|
@@ -4386,6 +4475,68 @@ var init_Pipeline = __esm({
|
|
|
4386
4475
|
// ---------------------------------------------------------------------------
|
|
4387
4476
|
// Card-space diagnostic
|
|
4388
4477
|
// ---------------------------------------------------------------------------
|
|
4478
|
+
/**
|
|
4479
|
+
* Commit-free forecast: score the user's full card space through the filter
|
|
4480
|
+
* chain and return the cards that are currently *reachable* (score >=
|
|
4481
|
+
* threshold), optionally nudged by caller-supplied hints and/or restricted
|
|
4482
|
+
* to cards the user hasn't seen yet.
|
|
4483
|
+
*
|
|
4484
|
+
* This is a GENERIC primitive — it returns scored, tag-hydrated cards and
|
|
4485
|
+
* stops there. It has no knowledge of any particular tag convention; callers
|
|
4486
|
+
* decide what the surviving cards mean (e.g. filter to their own "intro"
|
|
4487
|
+
* tag family). Nothing is written and no session is started.
|
|
4488
|
+
*
|
|
4489
|
+
* The optional `hints` are the "out-of-band kick": they run through the same
|
|
4490
|
+
* {@link applyHints} path a live replan uses, so the two semantics carry over —
|
|
4491
|
+
* - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
|
|
4492
|
+
* stays out), and
|
|
4493
|
+
* - `requireTags`/`requireCards` inject from the full pre-filter pool,
|
|
4494
|
+
* *bypassing* gating (use when you want a card regardless of reachability).
|
|
4495
|
+
* Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
|
|
4496
|
+
* user has already seen — pass `unseenOnly: false` if that matters.
|
|
4497
|
+
*
|
|
4498
|
+
* Cost note: like {@link diagnoseCardSpace}, this scans every card through the
|
|
4499
|
+
* filters, so it's heavier than a normal replan. Intended for one-shot
|
|
4500
|
+
* out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
|
|
4501
|
+
*
|
|
4502
|
+
* @param opts.hints Optional ephemeral hints to apply after the filter chain.
|
|
4503
|
+
* @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
|
|
4504
|
+
* @param opts.threshold Min score to count as reachable (default 0.10).
|
|
4505
|
+
* @param opts.limit Optional cap on results (already sorted desc).
|
|
4506
|
+
*/
|
|
4507
|
+
async forecast(opts) {
|
|
4508
|
+
const threshold = opts?.threshold ?? 0.1;
|
|
4509
|
+
const unseenOnly = opts?.unseenOnly ?? true;
|
|
4510
|
+
const courseId = this.course.getCourseID();
|
|
4511
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
4512
|
+
let cards = allCardIds.map((cardId) => ({
|
|
4513
|
+
cardId,
|
|
4514
|
+
courseId,
|
|
4515
|
+
score: 1,
|
|
4516
|
+
provenance: []
|
|
4517
|
+
}));
|
|
4518
|
+
cards = await this.hydrateTags(cards);
|
|
4519
|
+
const fullPool = cards.slice();
|
|
4520
|
+
const context = await this.buildContext();
|
|
4521
|
+
for (const filter of this.filters) {
|
|
4522
|
+
cards = await filter.transform(cards, context);
|
|
4523
|
+
}
|
|
4524
|
+
if (opts?.hints) {
|
|
4525
|
+
cards = this.applyHints(cards, opts.hints, fullPool);
|
|
4526
|
+
}
|
|
4527
|
+
cards = cards.filter((c) => c.score >= threshold);
|
|
4528
|
+
if (unseenOnly) {
|
|
4529
|
+
let encountered;
|
|
4530
|
+
try {
|
|
4531
|
+
encountered = new Set(await this.user.getSeenCards(courseId));
|
|
4532
|
+
} catch {
|
|
4533
|
+
encountered = /* @__PURE__ */ new Set();
|
|
4534
|
+
}
|
|
4535
|
+
cards = cards.filter((c) => !encountered.has(c.cardId));
|
|
4536
|
+
}
|
|
4537
|
+
cards.sort((a, b) => b.score - a.score);
|
|
4538
|
+
return opts?.limit ? cards.slice(0, opts.limit) : cards;
|
|
4539
|
+
}
|
|
4389
4540
|
/**
|
|
4390
4541
|
* Scan every card in the course through the filter chain and report
|
|
4391
4542
|
* how many are "well indicated" (score >= threshold) for the current user.
|
|
@@ -4650,6 +4801,7 @@ var init_3 = __esm({
|
|
|
4650
4801
|
"./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
|
|
4651
4802
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4652
4803
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4804
|
+
"./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
|
|
4653
4805
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
4654
4806
|
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4655
4807
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
@@ -4682,11 +4834,14 @@ __export(navigators_exports, {
|
|
|
4682
4834
|
NavigatorRole: () => NavigatorRole,
|
|
4683
4835
|
NavigatorRoles: () => NavigatorRoles,
|
|
4684
4836
|
Navigators: () => Navigators,
|
|
4837
|
+
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
4685
4838
|
diversityRerank: () => diversityRerank,
|
|
4839
|
+
getActivePipeline: () => getActivePipeline,
|
|
4686
4840
|
getCardOrigin: () => getCardOrigin,
|
|
4687
4841
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
4688
4842
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
4689
4843
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
4844
|
+
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
4690
4845
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
4691
4846
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
4692
4847
|
isFilter: () => isFilter,
|
|
@@ -4768,6 +4923,7 @@ var init_navigators = __esm({
|
|
|
4768
4923
|
"use strict";
|
|
4769
4924
|
init_diversityRerank();
|
|
4770
4925
|
init_PipelineDebugger();
|
|
4926
|
+
init_SrsDebugger();
|
|
4771
4927
|
init_logger();
|
|
4772
4928
|
init_();
|
|
4773
4929
|
init_2();
|