@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
|
@@ -716,6 +716,7 @@ __export(PipelineDebugger_exports, {
|
|
|
716
716
|
buildRunReport: () => buildRunReport,
|
|
717
717
|
captureRun: () => captureRun,
|
|
718
718
|
clearRunHistory: () => clearRunHistory,
|
|
719
|
+
getActivePipeline: () => getActivePipeline,
|
|
719
720
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
720
721
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
721
722
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -723,6 +724,9 @@ __export(PipelineDebugger_exports, {
|
|
|
723
724
|
function registerPipelineForDebug(pipeline) {
|
|
724
725
|
_activePipeline = pipeline;
|
|
725
726
|
}
|
|
727
|
+
function getActivePipeline() {
|
|
728
|
+
return _activePipeline;
|
|
729
|
+
}
|
|
726
730
|
function clearRunHistory() {
|
|
727
731
|
runHistory.length = 0;
|
|
728
732
|
}
|
|
@@ -1538,6 +1542,30 @@ Example:
|
|
|
1538
1542
|
}
|
|
1539
1543
|
});
|
|
1540
1544
|
|
|
1545
|
+
// src/core/navigators/SrsDebugger.ts
|
|
1546
|
+
var SrsDebugger_exports = {};
|
|
1547
|
+
__export(SrsDebugger_exports, {
|
|
1548
|
+
captureSrsBacklog: () => captureSrsBacklog,
|
|
1549
|
+
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
1550
|
+
getSrsBacklogDebug: () => getSrsBacklogDebug
|
|
1551
|
+
});
|
|
1552
|
+
function captureSrsBacklog(snapshot) {
|
|
1553
|
+
snapshots.set(snapshot.courseId, snapshot);
|
|
1554
|
+
}
|
|
1555
|
+
function getSrsBacklogDebug() {
|
|
1556
|
+
return [...snapshots.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
1557
|
+
}
|
|
1558
|
+
function clearSrsBacklogDebug() {
|
|
1559
|
+
snapshots.clear();
|
|
1560
|
+
}
|
|
1561
|
+
var snapshots;
|
|
1562
|
+
var init_SrsDebugger = __esm({
|
|
1563
|
+
"src/core/navigators/SrsDebugger.ts"() {
|
|
1564
|
+
"use strict";
|
|
1565
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
1566
|
+
}
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1541
1569
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
1542
1570
|
var CompositeGenerator_exports = {};
|
|
1543
1571
|
__export(CompositeGenerator_exports, {
|
|
@@ -1905,7 +1933,7 @@ function shuffleInPlace(arr) {
|
|
|
1905
1933
|
function pickTopByScore(cards, limit) {
|
|
1906
1934
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1907
1935
|
}
|
|
1908
|
-
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;
|
|
1936
|
+
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;
|
|
1909
1937
|
var init_prescribed = __esm({
|
|
1910
1938
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1911
1939
|
"use strict";
|
|
@@ -1922,6 +1950,9 @@ var init_prescribed = __esm({
|
|
|
1922
1950
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1923
1951
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
1924
1952
|
BASE_PRACTICE_SCORE = 1;
|
|
1953
|
+
PRACTICE_BASE_MULT = 2;
|
|
1954
|
+
MAX_PRACTICE_MULTIPLIER = 4;
|
|
1955
|
+
PRACTICE_STALENESS_BUMP_PER_DAY = 0.5;
|
|
1925
1956
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1926
1957
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1927
1958
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -1981,6 +2012,8 @@ var init_prescribed = __esm({
|
|
|
1981
2012
|
const emitted = [];
|
|
1982
2013
|
const emittedIds = /* @__PURE__ */ new Set();
|
|
1983
2014
|
const groupRuntimes = [];
|
|
2015
|
+
const priorPracticeDebt = progress.practiceDebt ?? {};
|
|
2016
|
+
const nextPracticeDebt = {};
|
|
1984
2017
|
for (const group of this.config.groups) {
|
|
1985
2018
|
const runtime = this.buildGroupRuntimeState({
|
|
1986
2019
|
group,
|
|
@@ -2038,10 +2071,13 @@ var init_prescribed = __esm({
|
|
|
2038
2071
|
userTagElo,
|
|
2039
2072
|
userGlobalElo,
|
|
2040
2073
|
activeIds,
|
|
2041
|
-
seenIds
|
|
2074
|
+
seenIds,
|
|
2075
|
+
priorPracticeDebt,
|
|
2076
|
+
nextPracticeDebt
|
|
2042
2077
|
});
|
|
2043
2078
|
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
2044
2079
|
}
|
|
2080
|
+
nextState.practiceDebt = nextPracticeDebt;
|
|
2045
2081
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2046
2082
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
2047
2083
|
boostTags: hintSummary.boostTags,
|
|
@@ -2375,9 +2411,16 @@ var init_prescribed = __esm({
|
|
|
2375
2411
|
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2376
2412
|
* into the candidate pool. It exists because global-ELO retrieval
|
|
2377
2413
|
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2378
|
-
* freshly-introduced skill — putting them in the pool here
|
|
2379
|
-
*
|
|
2380
|
-
*
|
|
2414
|
+
* freshly-introduced skill — putting them in the pool here guarantees presence.
|
|
2415
|
+
*
|
|
2416
|
+
* Emphasis is a **practice-debt pressure** (parallel to SRS backlog pressure):
|
|
2417
|
+
* cards score `base × multiplier`, where the multiplier starts at
|
|
2418
|
+
* PRACTICE_BASE_MULT (so a few reps land promptly post-intro, competing with
|
|
2419
|
+
* pressured reviews) and escalates by how long the debt has stayed open
|
|
2420
|
+
* (per-tag, time-based via `priorPracticeDebt`/`nextPracticeDebt`), clamped at
|
|
2421
|
+
* MAX_PRACTICE_MULTIPLIER. The debt is durable and self-discharges the instant
|
|
2422
|
+
* the skill reaches `practiceMinCount` — so this no longer relies on the
|
|
2423
|
+
* session-scoped intro boost to actually surface.
|
|
2381
2424
|
*
|
|
2382
2425
|
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2383
2426
|
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
@@ -2392,7 +2435,9 @@ var init_prescribed = __esm({
|
|
|
2392
2435
|
userTagElo,
|
|
2393
2436
|
userGlobalElo,
|
|
2394
2437
|
activeIds,
|
|
2395
|
-
seenIds
|
|
2438
|
+
seenIds,
|
|
2439
|
+
priorPracticeDebt,
|
|
2440
|
+
nextPracticeDebt
|
|
2396
2441
|
} = args;
|
|
2397
2442
|
const patterns = group.practiceTagPatterns ?? [];
|
|
2398
2443
|
if (patterns.length === 0) return [];
|
|
@@ -2402,6 +2447,20 @@ var init_prescribed = __esm({
|
|
|
2402
2447
|
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2403
2448
|
);
|
|
2404
2449
|
if (practiceTags.length === 0) return [];
|
|
2450
|
+
const now = Date.now();
|
|
2451
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2452
|
+
const tagMultiplier = /* @__PURE__ */ new Map();
|
|
2453
|
+
for (const tag of practiceTags) {
|
|
2454
|
+
const firstOwedAt = priorPracticeDebt[tag] ?? isoNow();
|
|
2455
|
+
nextPracticeDebt[tag] = firstOwedAt;
|
|
2456
|
+
const staleDays = Math.max(0, (now - new Date(firstOwedAt).getTime()) / DAY_MS);
|
|
2457
|
+
const mult = clamp(
|
|
2458
|
+
PRACTICE_BASE_MULT + staleDays * PRACTICE_STALENESS_BUMP_PER_DAY,
|
|
2459
|
+
PRACTICE_BASE_MULT,
|
|
2460
|
+
MAX_PRACTICE_MULTIPLIER
|
|
2461
|
+
);
|
|
2462
|
+
tagMultiplier.set(tag, mult);
|
|
2463
|
+
}
|
|
2405
2464
|
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2406
2465
|
supportTags: practiceTags,
|
|
2407
2466
|
cardsByTag,
|
|
@@ -2417,18 +2476,25 @@ var init_prescribed = __esm({
|
|
|
2417
2476
|
const cards = [];
|
|
2418
2477
|
for (const cardId of practiceCardIds) {
|
|
2419
2478
|
emittedIds.add(cardId);
|
|
2479
|
+
let mult = PRACTICE_BASE_MULT;
|
|
2480
|
+
for (const tag of practiceTags) {
|
|
2481
|
+
if (cardsByTag.get(tag)?.includes(cardId) ?? false) {
|
|
2482
|
+
mult = Math.max(mult, tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT);
|
|
2483
|
+
}
|
|
2484
|
+
}
|
|
2485
|
+
const score = BASE_PRACTICE_SCORE * mult;
|
|
2420
2486
|
cards.push({
|
|
2421
2487
|
cardId,
|
|
2422
2488
|
courseId,
|
|
2423
|
-
score
|
|
2489
|
+
score,
|
|
2424
2490
|
provenance: [
|
|
2425
2491
|
{
|
|
2426
2492
|
strategy: "prescribed",
|
|
2427
2493
|
strategyName: this.strategyName || this.name,
|
|
2428
2494
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2429
2495
|
action: "generated",
|
|
2430
|
-
score
|
|
2431
|
-
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2496
|
+
score,
|
|
2497
|
+
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}`
|
|
2432
2498
|
}
|
|
2433
2499
|
]
|
|
2434
2500
|
});
|
|
@@ -2602,14 +2668,15 @@ __export(srs_exports, {
|
|
|
2602
2668
|
default: () => SRSNavigator
|
|
2603
2669
|
});
|
|
2604
2670
|
import moment from "moment";
|
|
2605
|
-
var DEFAULT_HEALTHY_BACKLOG,
|
|
2671
|
+
var DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_MULTIPLIER, SRSNavigator;
|
|
2606
2672
|
var init_srs = __esm({
|
|
2607
2673
|
"src/core/navigators/generators/srs.ts"() {
|
|
2608
2674
|
"use strict";
|
|
2609
2675
|
init_navigators();
|
|
2676
|
+
init_SrsDebugger();
|
|
2610
2677
|
init_logger();
|
|
2611
2678
|
DEFAULT_HEALTHY_BACKLOG = 20;
|
|
2612
|
-
|
|
2679
|
+
MAX_BACKLOG_MULTIPLIER = 2;
|
|
2613
2680
|
SRSNavigator = class extends ContentNavigator {
|
|
2614
2681
|
/** Human-readable name for CardGenerator interface */
|
|
2615
2682
|
name;
|
|
@@ -2676,9 +2743,18 @@ var init_srs = __esm({
|
|
|
2676
2743
|
}
|
|
2677
2744
|
}
|
|
2678
2745
|
}
|
|
2679
|
-
const
|
|
2746
|
+
const backlogMultiplier = this.computeBacklogMultiplier(dueReviews.length);
|
|
2747
|
+
const notDue = reviews.filter((r) => !now.isAfter(moment.utc(r.reviewTime)));
|
|
2748
|
+
let nextDueIn = null;
|
|
2749
|
+
if (notDue.length > 0) {
|
|
2750
|
+
const next = notDue.reduce(
|
|
2751
|
+
(a, b) => moment.utc(a.reviewTime).isBefore(moment.utc(b.reviewTime)) ? a : b
|
|
2752
|
+
);
|
|
2753
|
+
const until = moment.duration(moment.utc(next.reviewTime).diff(now));
|
|
2754
|
+
nextDueIn = until.asHours() < 1 ? `${Math.round(until.asMinutes())}m` : until.asHours() < 24 ? `${Math.round(until.asHours())}h` : `${Math.round(until.asDays())}d`;
|
|
2755
|
+
}
|
|
2680
2756
|
if (dueReviews.length > 0) {
|
|
2681
|
-
const pressureNote =
|
|
2757
|
+
const pressureNote = backlogMultiplier > 1 ? ` [backlog pressure: \xD7${backlogMultiplier.toFixed(2)}]` : ` [healthy backlog]`;
|
|
2682
2758
|
logger.info(
|
|
2683
2759
|
`[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
|
|
2684
2760
|
);
|
|
@@ -2697,7 +2773,7 @@ var init_srs = __esm({
|
|
|
2697
2773
|
logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
|
|
2698
2774
|
}
|
|
2699
2775
|
const scored = dueReviews.map((review) => {
|
|
2700
|
-
const { score, reason } = this.computeUrgencyScore(review, now,
|
|
2776
|
+
const { score, reason } = this.computeUrgencyScore(review, now, backlogMultiplier);
|
|
2701
2777
|
return {
|
|
2702
2778
|
cardId: review.cardId,
|
|
2703
2779
|
courseId: review.courseId,
|
|
@@ -2715,30 +2791,42 @@ var init_srs = __esm({
|
|
|
2715
2791
|
]
|
|
2716
2792
|
};
|
|
2717
2793
|
});
|
|
2718
|
-
|
|
2794
|
+
const sorted = scored.sort((a, b) => b.score - a.score);
|
|
2795
|
+
captureSrsBacklog({
|
|
2796
|
+
courseId,
|
|
2797
|
+
scheduledTotal: reviews.length,
|
|
2798
|
+
dueNow: dueReviews.length,
|
|
2799
|
+
healthyBacklog: this.healthyBacklog,
|
|
2800
|
+
backlogMultiplier,
|
|
2801
|
+
maxBacklogMultiplier: MAX_BACKLOG_MULTIPLIER,
|
|
2802
|
+
topReviewScore: sorted.length > 0 ? sorted[0].score : null,
|
|
2803
|
+
nextDueIn,
|
|
2804
|
+
timestamp: Date.now()
|
|
2805
|
+
});
|
|
2806
|
+
return { cards: sorted.slice(0, limit) };
|
|
2719
2807
|
}
|
|
2720
2808
|
/**
|
|
2721
|
-
* Compute backlog pressure based on number of due reviews.
|
|
2809
|
+
* Compute the multiplicative backlog pressure based on number of due reviews.
|
|
2722
2810
|
*
|
|
2723
|
-
*
|
|
2724
|
-
* and
|
|
2811
|
+
* ×1.0 at or below the healthy threshold (no boost), increasing linearly above
|
|
2812
|
+
* it and maxing out at MAX_BACKLOG_MULTIPLIER at 3× the healthy backlog.
|
|
2725
2813
|
*
|
|
2726
|
-
* Examples (with default healthyBacklog=20):
|
|
2727
|
-
* - 10 due reviews →
|
|
2728
|
-
* - 20 due reviews →
|
|
2729
|
-
* - 40 due reviews →
|
|
2730
|
-
* - 60 due reviews →
|
|
2814
|
+
* Examples (with default healthyBacklog=20, MAX_BACKLOG_MULTIPLIER=2.0):
|
|
2815
|
+
* - 10 due reviews → ×1.00 (healthy)
|
|
2816
|
+
* - 20 due reviews → ×1.00 (at threshold)
|
|
2817
|
+
* - 40 due reviews → ×1.50 (2x threshold)
|
|
2818
|
+
* - 60 due reviews → ×2.00 (3x threshold, maxed)
|
|
2731
2819
|
*
|
|
2732
2820
|
* @param dueCount - Number of reviews currently due
|
|
2733
|
-
* @returns
|
|
2821
|
+
* @returns Multiplier applied to review urgency (1.0 to MAX_BACKLOG_MULTIPLIER)
|
|
2734
2822
|
*/
|
|
2735
|
-
|
|
2823
|
+
computeBacklogMultiplier(dueCount) {
|
|
2736
2824
|
if (dueCount <= this.healthyBacklog) {
|
|
2737
|
-
return
|
|
2825
|
+
return 1;
|
|
2738
2826
|
}
|
|
2739
2827
|
const excess = dueCount - this.healthyBacklog;
|
|
2740
|
-
const
|
|
2741
|
-
return Math.min(
|
|
2828
|
+
const multiplier = 1 + excess / this.healthyBacklog * ((MAX_BACKLOG_MULTIPLIER - 1) / 2);
|
|
2829
|
+
return Math.min(MAX_BACKLOG_MULTIPLIER, multiplier);
|
|
2742
2830
|
}
|
|
2743
2831
|
/**
|
|
2744
2832
|
* Compute urgency score for a review card.
|
|
@@ -2753,19 +2841,20 @@ var init_srs = __esm({
|
|
|
2753
2841
|
* - 30 days (720h) → ~0.56
|
|
2754
2842
|
* - 180 days → ~0.30
|
|
2755
2843
|
*
|
|
2756
|
-
* 3. Backlog pressure = global
|
|
2757
|
-
*
|
|
2758
|
-
* - At 2x healthy: +0.25
|
|
2759
|
-
* - At 3x+ healthy: +0.50 (max)
|
|
2844
|
+
* 3. Backlog pressure = global *multiplier* when review backlog exceeds the
|
|
2845
|
+
* healthy threshold (×1.0 healthy → up to MAX_BACKLOG_MULTIPLIER at 3×).
|
|
2760
2846
|
*
|
|
2761
|
-
* Combined: base 0.5 +
|
|
2762
|
-
*
|
|
2847
|
+
* Combined: (base 0.5 + urgency factors * 0.45) × backlog multiplier.
|
|
2848
|
+
* Per-card range before pressure: ~0.57–0.95. NOT clamped to 1.0 — under a
|
|
2849
|
+
* heavy backlog reviews scale onto the open scale to compete with (and exceed)
|
|
2850
|
+
* new cards; what keeps them from running away is the bounded multiplier, not
|
|
2851
|
+
* a hard ceiling.
|
|
2763
2852
|
*
|
|
2764
2853
|
* @param review - The scheduled card to score
|
|
2765
2854
|
* @param now - Current time
|
|
2766
|
-
* @param
|
|
2855
|
+
* @param backlogMultiplier - Pre-computed backlog multiplier (1.0 to MAX_BACKLOG_MULTIPLIER)
|
|
2767
2856
|
*/
|
|
2768
|
-
computeUrgencyScore(review, now,
|
|
2857
|
+
computeUrgencyScore(review, now, backlogMultiplier) {
|
|
2769
2858
|
const scheduledAt = moment.utc(review.scheduledAt);
|
|
2770
2859
|
const due = moment.utc(review.reviewTime);
|
|
2771
2860
|
const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
|
|
@@ -2775,15 +2864,15 @@ var init_srs = __esm({
|
|
|
2775
2864
|
const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
|
|
2776
2865
|
const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
|
|
2777
2866
|
const baseScore = 0.5 + urgency * 0.45;
|
|
2778
|
-
const score =
|
|
2867
|
+
const score = baseScore * backlogMultiplier;
|
|
2779
2868
|
const reasonParts = [
|
|
2780
2869
|
`${Math.round(hoursOverdue)}h overdue`,
|
|
2781
2870
|
`interval: ${Math.round(intervalHours)}h`,
|
|
2782
2871
|
`relative: ${relativeOverdue.toFixed(2)}`,
|
|
2783
2872
|
`recency: ${recencyFactor.toFixed(2)}`
|
|
2784
2873
|
];
|
|
2785
|
-
if (
|
|
2786
|
-
reasonParts.push(`backlog:
|
|
2874
|
+
if (backlogMultiplier > 1) {
|
|
2875
|
+
reasonParts.push(`backlog: \xD7${backlogMultiplier.toFixed(2)}`);
|
|
2787
2876
|
}
|
|
2788
2877
|
reasonParts.push("review");
|
|
2789
2878
|
const reason = reasonParts.join(", ");
|
|
@@ -4509,6 +4598,68 @@ var init_Pipeline = __esm({
|
|
|
4509
4598
|
// ---------------------------------------------------------------------------
|
|
4510
4599
|
// Card-space diagnostic
|
|
4511
4600
|
// ---------------------------------------------------------------------------
|
|
4601
|
+
/**
|
|
4602
|
+
* Commit-free forecast: score the user's full card space through the filter
|
|
4603
|
+
* chain and return the cards that are currently *reachable* (score >=
|
|
4604
|
+
* threshold), optionally nudged by caller-supplied hints and/or restricted
|
|
4605
|
+
* to cards the user hasn't seen yet.
|
|
4606
|
+
*
|
|
4607
|
+
* This is a GENERIC primitive — it returns scored, tag-hydrated cards and
|
|
4608
|
+
* stops there. It has no knowledge of any particular tag convention; callers
|
|
4609
|
+
* decide what the surviving cards mean (e.g. filter to their own "intro"
|
|
4610
|
+
* tag family). Nothing is written and no session is started.
|
|
4611
|
+
*
|
|
4612
|
+
* The optional `hints` are the "out-of-band kick": they run through the same
|
|
4613
|
+
* {@link applyHints} path a live replan uses, so the two semantics carry over —
|
|
4614
|
+
* - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
|
|
4615
|
+
* stays out), and
|
|
4616
|
+
* - `requireTags`/`requireCards` inject from the full pre-filter pool,
|
|
4617
|
+
* *bypassing* gating (use when you want a card regardless of reachability).
|
|
4618
|
+
* Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
|
|
4619
|
+
* user has already seen — pass `unseenOnly: false` if that matters.
|
|
4620
|
+
*
|
|
4621
|
+
* Cost note: like {@link diagnoseCardSpace}, this scans every card through the
|
|
4622
|
+
* filters, so it's heavier than a normal replan. Intended for one-shot
|
|
4623
|
+
* out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
|
|
4624
|
+
*
|
|
4625
|
+
* @param opts.hints Optional ephemeral hints to apply after the filter chain.
|
|
4626
|
+
* @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
|
|
4627
|
+
* @param opts.threshold Min score to count as reachable (default 0.10).
|
|
4628
|
+
* @param opts.limit Optional cap on results (already sorted desc).
|
|
4629
|
+
*/
|
|
4630
|
+
async forecast(opts) {
|
|
4631
|
+
const threshold = opts?.threshold ?? 0.1;
|
|
4632
|
+
const unseenOnly = opts?.unseenOnly ?? true;
|
|
4633
|
+
const courseId = this.course.getCourseID();
|
|
4634
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
4635
|
+
let cards = allCardIds.map((cardId) => ({
|
|
4636
|
+
cardId,
|
|
4637
|
+
courseId,
|
|
4638
|
+
score: 1,
|
|
4639
|
+
provenance: []
|
|
4640
|
+
}));
|
|
4641
|
+
cards = await this.hydrateTags(cards);
|
|
4642
|
+
const fullPool = cards.slice();
|
|
4643
|
+
const context = await this.buildContext();
|
|
4644
|
+
for (const filter of this.filters) {
|
|
4645
|
+
cards = await filter.transform(cards, context);
|
|
4646
|
+
}
|
|
4647
|
+
if (opts?.hints) {
|
|
4648
|
+
cards = this.applyHints(cards, opts.hints, fullPool);
|
|
4649
|
+
}
|
|
4650
|
+
cards = cards.filter((c) => c.score >= threshold);
|
|
4651
|
+
if (unseenOnly) {
|
|
4652
|
+
let encountered;
|
|
4653
|
+
try {
|
|
4654
|
+
encountered = new Set(await this.user.getSeenCards(courseId));
|
|
4655
|
+
} catch {
|
|
4656
|
+
encountered = /* @__PURE__ */ new Set();
|
|
4657
|
+
}
|
|
4658
|
+
cards = cards.filter((c) => !encountered.has(c.cardId));
|
|
4659
|
+
}
|
|
4660
|
+
cards.sort((a, b) => b.score - a.score);
|
|
4661
|
+
return opts?.limit ? cards.slice(0, opts.limit) : cards;
|
|
4662
|
+
}
|
|
4512
4663
|
/**
|
|
4513
4664
|
* Scan every card in the course through the filter chain and report
|
|
4514
4665
|
* how many are "well indicated" (score >= threshold) for the current user.
|
|
@@ -4773,6 +4924,7 @@ var init_3 = __esm({
|
|
|
4773
4924
|
"./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
|
|
4774
4925
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4775
4926
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4927
|
+
"./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
|
|
4776
4928
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
4777
4929
|
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4778
4930
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
@@ -4805,11 +4957,14 @@ __export(navigators_exports, {
|
|
|
4805
4957
|
NavigatorRole: () => NavigatorRole,
|
|
4806
4958
|
NavigatorRoles: () => NavigatorRoles,
|
|
4807
4959
|
Navigators: () => Navigators,
|
|
4960
|
+
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
4808
4961
|
diversityRerank: () => diversityRerank,
|
|
4962
|
+
getActivePipeline: () => getActivePipeline,
|
|
4809
4963
|
getCardOrigin: () => getCardOrigin,
|
|
4810
4964
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
4811
4965
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
4812
4966
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
4967
|
+
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
4813
4968
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
4814
4969
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
4815
4970
|
isFilter: () => isFilter,
|
|
@@ -4891,6 +5046,7 @@ var init_navigators = __esm({
|
|
|
4891
5046
|
"use strict";
|
|
4892
5047
|
init_diversityRerank();
|
|
4893
5048
|
init_PipelineDebugger();
|
|
5049
|
+
init_SrsDebugger();
|
|
4894
5050
|
init_logger();
|
|
4895
5051
|
init_();
|
|
4896
5052
|
init_2();
|