@vue-skuilder/db 0.2.8 → 0.2.10
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 +29 -4
- package/dist/core/index.d.ts +29 -4
- package/dist/core/index.js +135 -41
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +133 -41
- 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 +131 -41
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +131 -41
- 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 +128 -39
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +128 -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 +374 -253
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +372 -253
- 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/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 +5 -0
- package/src/impl/couch/courseDB.ts +11 -2
- package/src/study/SessionController.ts +260 -249
- package/src/study/SessionDebugger.ts +15 -25
- package/src/study/SessionOverlay.ts +108 -13
package/dist/index.js
CHANGED
|
@@ -1793,6 +1793,30 @@ Example:
|
|
|
1793
1793
|
}
|
|
1794
1794
|
});
|
|
1795
1795
|
|
|
1796
|
+
// src/core/navigators/SrsDebugger.ts
|
|
1797
|
+
var SrsDebugger_exports = {};
|
|
1798
|
+
__export(SrsDebugger_exports, {
|
|
1799
|
+
captureSrsBacklog: () => captureSrsBacklog,
|
|
1800
|
+
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
1801
|
+
getSrsBacklogDebug: () => getSrsBacklogDebug
|
|
1802
|
+
});
|
|
1803
|
+
function captureSrsBacklog(snapshot) {
|
|
1804
|
+
snapshots.set(snapshot.courseId, snapshot);
|
|
1805
|
+
}
|
|
1806
|
+
function getSrsBacklogDebug() {
|
|
1807
|
+
return [...snapshots.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
1808
|
+
}
|
|
1809
|
+
function clearSrsBacklogDebug() {
|
|
1810
|
+
snapshots.clear();
|
|
1811
|
+
}
|
|
1812
|
+
var snapshots;
|
|
1813
|
+
var init_SrsDebugger = __esm({
|
|
1814
|
+
"src/core/navigators/SrsDebugger.ts"() {
|
|
1815
|
+
"use strict";
|
|
1816
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
1817
|
+
}
|
|
1818
|
+
});
|
|
1819
|
+
|
|
1796
1820
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
1797
1821
|
var CompositeGenerator_exports = {};
|
|
1798
1822
|
__export(CompositeGenerator_exports, {
|
|
@@ -2160,7 +2184,7 @@ function shuffleInPlace(arr) {
|
|
|
2160
2184
|
function pickTopByScore(cards, limit) {
|
|
2161
2185
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
2162
2186
|
}
|
|
2163
|
-
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;
|
|
2187
|
+
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;
|
|
2164
2188
|
var init_prescribed = __esm({
|
|
2165
2189
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
2166
2190
|
"use strict";
|
|
@@ -2177,6 +2201,9 @@ var init_prescribed = __esm({
|
|
|
2177
2201
|
BASE_SUPPORT_SCORE = 0.8;
|
|
2178
2202
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
2179
2203
|
BASE_PRACTICE_SCORE = 1;
|
|
2204
|
+
PRACTICE_BASE_MULT = 2;
|
|
2205
|
+
MAX_PRACTICE_MULTIPLIER = 4;
|
|
2206
|
+
PRACTICE_STALENESS_BUMP_PER_DAY = 0.5;
|
|
2180
2207
|
MAX_TARGET_MULTIPLIER = 8;
|
|
2181
2208
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
2182
2209
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -2236,6 +2263,8 @@ var init_prescribed = __esm({
|
|
|
2236
2263
|
const emitted = [];
|
|
2237
2264
|
const emittedIds = /* @__PURE__ */ new Set();
|
|
2238
2265
|
const groupRuntimes = [];
|
|
2266
|
+
const priorPracticeDebt = progress.practiceDebt ?? {};
|
|
2267
|
+
const nextPracticeDebt = {};
|
|
2239
2268
|
for (const group of this.config.groups) {
|
|
2240
2269
|
const runtime = this.buildGroupRuntimeState({
|
|
2241
2270
|
group,
|
|
@@ -2293,10 +2322,13 @@ var init_prescribed = __esm({
|
|
|
2293
2322
|
userTagElo,
|
|
2294
2323
|
userGlobalElo,
|
|
2295
2324
|
activeIds,
|
|
2296
|
-
seenIds
|
|
2325
|
+
seenIds,
|
|
2326
|
+
priorPracticeDebt,
|
|
2327
|
+
nextPracticeDebt
|
|
2297
2328
|
});
|
|
2298
2329
|
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
2299
2330
|
}
|
|
2331
|
+
nextState.practiceDebt = nextPracticeDebt;
|
|
2300
2332
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2301
2333
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
2302
2334
|
boostTags: hintSummary.boostTags,
|
|
@@ -2630,9 +2662,16 @@ var init_prescribed = __esm({
|
|
|
2630
2662
|
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2631
2663
|
* into the candidate pool. It exists because global-ELO retrieval
|
|
2632
2664
|
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2633
|
-
* freshly-introduced skill — putting them in the pool here
|
|
2634
|
-
*
|
|
2635
|
-
*
|
|
2665
|
+
* freshly-introduced skill — putting them in the pool here guarantees presence.
|
|
2666
|
+
*
|
|
2667
|
+
* Emphasis is a **practice-debt pressure** (parallel to SRS backlog pressure):
|
|
2668
|
+
* cards score `base × multiplier`, where the multiplier starts at
|
|
2669
|
+
* PRACTICE_BASE_MULT (so a few reps land promptly post-intro, competing with
|
|
2670
|
+
* pressured reviews) and escalates by how long the debt has stayed open
|
|
2671
|
+
* (per-tag, time-based via `priorPracticeDebt`/`nextPracticeDebt`), clamped at
|
|
2672
|
+
* MAX_PRACTICE_MULTIPLIER. The debt is durable and self-discharges the instant
|
|
2673
|
+
* the skill reaches `practiceMinCount` — so this no longer relies on the
|
|
2674
|
+
* session-scoped intro boost to actually surface.
|
|
2636
2675
|
*
|
|
2637
2676
|
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2638
2677
|
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
@@ -2647,7 +2686,9 @@ var init_prescribed = __esm({
|
|
|
2647
2686
|
userTagElo,
|
|
2648
2687
|
userGlobalElo,
|
|
2649
2688
|
activeIds,
|
|
2650
|
-
seenIds
|
|
2689
|
+
seenIds,
|
|
2690
|
+
priorPracticeDebt,
|
|
2691
|
+
nextPracticeDebt
|
|
2651
2692
|
} = args;
|
|
2652
2693
|
const patterns = group.practiceTagPatterns ?? [];
|
|
2653
2694
|
if (patterns.length === 0) return [];
|
|
@@ -2657,6 +2698,20 @@ var init_prescribed = __esm({
|
|
|
2657
2698
|
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2658
2699
|
);
|
|
2659
2700
|
if (practiceTags.length === 0) return [];
|
|
2701
|
+
const now = Date.now();
|
|
2702
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2703
|
+
const tagMultiplier = /* @__PURE__ */ new Map();
|
|
2704
|
+
for (const tag of practiceTags) {
|
|
2705
|
+
const firstOwedAt = priorPracticeDebt[tag] ?? isoNow();
|
|
2706
|
+
nextPracticeDebt[tag] = firstOwedAt;
|
|
2707
|
+
const staleDays = Math.max(0, (now - new Date(firstOwedAt).getTime()) / DAY_MS);
|
|
2708
|
+
const mult = clamp(
|
|
2709
|
+
PRACTICE_BASE_MULT + staleDays * PRACTICE_STALENESS_BUMP_PER_DAY,
|
|
2710
|
+
PRACTICE_BASE_MULT,
|
|
2711
|
+
MAX_PRACTICE_MULTIPLIER
|
|
2712
|
+
);
|
|
2713
|
+
tagMultiplier.set(tag, mult);
|
|
2714
|
+
}
|
|
2660
2715
|
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2661
2716
|
supportTags: practiceTags,
|
|
2662
2717
|
cardsByTag,
|
|
@@ -2672,18 +2727,25 @@ var init_prescribed = __esm({
|
|
|
2672
2727
|
const cards = [];
|
|
2673
2728
|
for (const cardId of practiceCardIds) {
|
|
2674
2729
|
emittedIds.add(cardId);
|
|
2730
|
+
let mult = PRACTICE_BASE_MULT;
|
|
2731
|
+
for (const tag of practiceTags) {
|
|
2732
|
+
if (cardsByTag.get(tag)?.includes(cardId) ?? false) {
|
|
2733
|
+
mult = Math.max(mult, tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT);
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2736
|
+
const score = BASE_PRACTICE_SCORE * mult;
|
|
2675
2737
|
cards.push({
|
|
2676
2738
|
cardId,
|
|
2677
2739
|
courseId,
|
|
2678
|
-
score
|
|
2740
|
+
score,
|
|
2679
2741
|
provenance: [
|
|
2680
2742
|
{
|
|
2681
2743
|
strategy: "prescribed",
|
|
2682
2744
|
strategyName: this.strategyName || this.name,
|
|
2683
2745
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2684
2746
|
action: "generated",
|
|
2685
|
-
score
|
|
2686
|
-
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2747
|
+
score,
|
|
2748
|
+
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}`
|
|
2687
2749
|
}
|
|
2688
2750
|
]
|
|
2689
2751
|
});
|
|
@@ -2856,15 +2918,16 @@ var srs_exports = {};
|
|
|
2856
2918
|
__export(srs_exports, {
|
|
2857
2919
|
default: () => SRSNavigator
|
|
2858
2920
|
});
|
|
2859
|
-
var import_moment3, DEFAULT_HEALTHY_BACKLOG,
|
|
2921
|
+
var import_moment3, DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_MULTIPLIER, SRSNavigator;
|
|
2860
2922
|
var init_srs = __esm({
|
|
2861
2923
|
"src/core/navigators/generators/srs.ts"() {
|
|
2862
2924
|
"use strict";
|
|
2863
2925
|
import_moment3 = __toESM(require("moment"), 1);
|
|
2864
2926
|
init_navigators();
|
|
2927
|
+
init_SrsDebugger();
|
|
2865
2928
|
init_logger();
|
|
2866
2929
|
DEFAULT_HEALTHY_BACKLOG = 20;
|
|
2867
|
-
|
|
2930
|
+
MAX_BACKLOG_MULTIPLIER = 2;
|
|
2868
2931
|
SRSNavigator = class extends ContentNavigator {
|
|
2869
2932
|
/** Human-readable name for CardGenerator interface */
|
|
2870
2933
|
name;
|
|
@@ -2931,9 +2994,18 @@ var init_srs = __esm({
|
|
|
2931
2994
|
}
|
|
2932
2995
|
}
|
|
2933
2996
|
}
|
|
2934
|
-
const
|
|
2997
|
+
const backlogMultiplier = this.computeBacklogMultiplier(dueReviews.length);
|
|
2998
|
+
const notDue = reviews.filter((r) => !now.isAfter(import_moment3.default.utc(r.reviewTime)));
|
|
2999
|
+
let nextDueIn = null;
|
|
3000
|
+
if (notDue.length > 0) {
|
|
3001
|
+
const next = notDue.reduce(
|
|
3002
|
+
(a, b) => import_moment3.default.utc(a.reviewTime).isBefore(import_moment3.default.utc(b.reviewTime)) ? a : b
|
|
3003
|
+
);
|
|
3004
|
+
const until = import_moment3.default.duration(import_moment3.default.utc(next.reviewTime).diff(now));
|
|
3005
|
+
nextDueIn = until.asHours() < 1 ? `${Math.round(until.asMinutes())}m` : until.asHours() < 24 ? `${Math.round(until.asHours())}h` : `${Math.round(until.asDays())}d`;
|
|
3006
|
+
}
|
|
2935
3007
|
if (dueReviews.length > 0) {
|
|
2936
|
-
const pressureNote =
|
|
3008
|
+
const pressureNote = backlogMultiplier > 1 ? ` [backlog pressure: \xD7${backlogMultiplier.toFixed(2)}]` : ` [healthy backlog]`;
|
|
2937
3009
|
logger.info(
|
|
2938
3010
|
`[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
|
|
2939
3011
|
);
|
|
@@ -2952,7 +3024,7 @@ var init_srs = __esm({
|
|
|
2952
3024
|
logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
|
|
2953
3025
|
}
|
|
2954
3026
|
const scored = dueReviews.map((review) => {
|
|
2955
|
-
const { score, reason } = this.computeUrgencyScore(review, now,
|
|
3027
|
+
const { score, reason } = this.computeUrgencyScore(review, now, backlogMultiplier);
|
|
2956
3028
|
return {
|
|
2957
3029
|
cardId: review.cardId,
|
|
2958
3030
|
courseId: review.courseId,
|
|
@@ -2970,30 +3042,42 @@ var init_srs = __esm({
|
|
|
2970
3042
|
]
|
|
2971
3043
|
};
|
|
2972
3044
|
});
|
|
2973
|
-
|
|
3045
|
+
const sorted = scored.sort((a, b) => b.score - a.score);
|
|
3046
|
+
captureSrsBacklog({
|
|
3047
|
+
courseId,
|
|
3048
|
+
scheduledTotal: reviews.length,
|
|
3049
|
+
dueNow: dueReviews.length,
|
|
3050
|
+
healthyBacklog: this.healthyBacklog,
|
|
3051
|
+
backlogMultiplier,
|
|
3052
|
+
maxBacklogMultiplier: MAX_BACKLOG_MULTIPLIER,
|
|
3053
|
+
topReviewScore: sorted.length > 0 ? sorted[0].score : null,
|
|
3054
|
+
nextDueIn,
|
|
3055
|
+
timestamp: Date.now()
|
|
3056
|
+
});
|
|
3057
|
+
return { cards: sorted.slice(0, limit) };
|
|
2974
3058
|
}
|
|
2975
3059
|
/**
|
|
2976
|
-
* Compute backlog pressure based on number of due reviews.
|
|
3060
|
+
* Compute the multiplicative backlog pressure based on number of due reviews.
|
|
2977
3061
|
*
|
|
2978
|
-
*
|
|
2979
|
-
* and
|
|
3062
|
+
* ×1.0 at or below the healthy threshold (no boost), increasing linearly above
|
|
3063
|
+
* it and maxing out at MAX_BACKLOG_MULTIPLIER at 3× the healthy backlog.
|
|
2980
3064
|
*
|
|
2981
|
-
* Examples (with default healthyBacklog=20):
|
|
2982
|
-
* - 10 due reviews →
|
|
2983
|
-
* - 20 due reviews →
|
|
2984
|
-
* - 40 due reviews →
|
|
2985
|
-
* - 60 due reviews →
|
|
3065
|
+
* Examples (with default healthyBacklog=20, MAX_BACKLOG_MULTIPLIER=2.0):
|
|
3066
|
+
* - 10 due reviews → ×1.00 (healthy)
|
|
3067
|
+
* - 20 due reviews → ×1.00 (at threshold)
|
|
3068
|
+
* - 40 due reviews → ×1.50 (2x threshold)
|
|
3069
|
+
* - 60 due reviews → ×2.00 (3x threshold, maxed)
|
|
2986
3070
|
*
|
|
2987
3071
|
* @param dueCount - Number of reviews currently due
|
|
2988
|
-
* @returns
|
|
3072
|
+
* @returns Multiplier applied to review urgency (1.0 to MAX_BACKLOG_MULTIPLIER)
|
|
2989
3073
|
*/
|
|
2990
|
-
|
|
3074
|
+
computeBacklogMultiplier(dueCount) {
|
|
2991
3075
|
if (dueCount <= this.healthyBacklog) {
|
|
2992
|
-
return
|
|
3076
|
+
return 1;
|
|
2993
3077
|
}
|
|
2994
3078
|
const excess = dueCount - this.healthyBacklog;
|
|
2995
|
-
const
|
|
2996
|
-
return Math.min(
|
|
3079
|
+
const multiplier = 1 + excess / this.healthyBacklog * ((MAX_BACKLOG_MULTIPLIER - 1) / 2);
|
|
3080
|
+
return Math.min(MAX_BACKLOG_MULTIPLIER, multiplier);
|
|
2997
3081
|
}
|
|
2998
3082
|
/**
|
|
2999
3083
|
* Compute urgency score for a review card.
|
|
@@ -3008,19 +3092,20 @@ var init_srs = __esm({
|
|
|
3008
3092
|
* - 30 days (720h) → ~0.56
|
|
3009
3093
|
* - 180 days → ~0.30
|
|
3010
3094
|
*
|
|
3011
|
-
* 3. Backlog pressure = global
|
|
3012
|
-
*
|
|
3013
|
-
* - At 2x healthy: +0.25
|
|
3014
|
-
* - At 3x+ healthy: +0.50 (max)
|
|
3095
|
+
* 3. Backlog pressure = global *multiplier* when review backlog exceeds the
|
|
3096
|
+
* healthy threshold (×1.0 healthy → up to MAX_BACKLOG_MULTIPLIER at 3×).
|
|
3015
3097
|
*
|
|
3016
|
-
* Combined: base 0.5 +
|
|
3017
|
-
*
|
|
3098
|
+
* Combined: (base 0.5 + urgency factors * 0.45) × backlog multiplier.
|
|
3099
|
+
* Per-card range before pressure: ~0.57–0.95. NOT clamped to 1.0 — under a
|
|
3100
|
+
* heavy backlog reviews scale onto the open scale to compete with (and exceed)
|
|
3101
|
+
* new cards; what keeps them from running away is the bounded multiplier, not
|
|
3102
|
+
* a hard ceiling.
|
|
3018
3103
|
*
|
|
3019
3104
|
* @param review - The scheduled card to score
|
|
3020
3105
|
* @param now - Current time
|
|
3021
|
-
* @param
|
|
3106
|
+
* @param backlogMultiplier - Pre-computed backlog multiplier (1.0 to MAX_BACKLOG_MULTIPLIER)
|
|
3022
3107
|
*/
|
|
3023
|
-
computeUrgencyScore(review, now,
|
|
3108
|
+
computeUrgencyScore(review, now, backlogMultiplier) {
|
|
3024
3109
|
const scheduledAt = import_moment3.default.utc(review.scheduledAt);
|
|
3025
3110
|
const due = import_moment3.default.utc(review.reviewTime);
|
|
3026
3111
|
const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
|
|
@@ -3030,15 +3115,15 @@ var init_srs = __esm({
|
|
|
3030
3115
|
const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
|
|
3031
3116
|
const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
|
|
3032
3117
|
const baseScore = 0.5 + urgency * 0.45;
|
|
3033
|
-
const score =
|
|
3118
|
+
const score = baseScore * backlogMultiplier;
|
|
3034
3119
|
const reasonParts = [
|
|
3035
3120
|
`${Math.round(hoursOverdue)}h overdue`,
|
|
3036
3121
|
`interval: ${Math.round(intervalHours)}h`,
|
|
3037
3122
|
`relative: ${relativeOverdue.toFixed(2)}`,
|
|
3038
3123
|
`recency: ${recencyFactor.toFixed(2)}`
|
|
3039
3124
|
];
|
|
3040
|
-
if (
|
|
3041
|
-
reasonParts.push(`backlog:
|
|
3125
|
+
if (backlogMultiplier > 1) {
|
|
3126
|
+
reasonParts.push(`backlog: \xD7${backlogMultiplier.toFixed(2)}`);
|
|
3042
3127
|
}
|
|
3043
3128
|
reasonParts.push("review");
|
|
3044
3129
|
const reason = reasonParts.join(", ");
|
|
@@ -5336,6 +5421,7 @@ var init_3 = __esm({
|
|
|
5336
5421
|
"./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
|
|
5337
5422
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
5338
5423
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
5424
|
+
"./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
|
|
5339
5425
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5340
5426
|
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
5341
5427
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
@@ -5368,12 +5454,14 @@ __export(navigators_exports, {
|
|
|
5368
5454
|
NavigatorRole: () => NavigatorRole,
|
|
5369
5455
|
NavigatorRoles: () => NavigatorRoles,
|
|
5370
5456
|
Navigators: () => Navigators,
|
|
5457
|
+
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
5371
5458
|
diversityRerank: () => diversityRerank,
|
|
5372
5459
|
getActivePipeline: () => getActivePipeline,
|
|
5373
5460
|
getCardOrigin: () => getCardOrigin,
|
|
5374
5461
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
5375
5462
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
5376
5463
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
5464
|
+
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
5377
5465
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
5378
5466
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
5379
5467
|
isFilter: () => isFilter,
|
|
@@ -5455,6 +5543,7 @@ var init_navigators = __esm({
|
|
|
5455
5543
|
"use strict";
|
|
5456
5544
|
init_diversityRerank();
|
|
5457
5545
|
init_PipelineDebugger();
|
|
5546
|
+
init_SrsDebugger();
|
|
5458
5547
|
init_logger();
|
|
5459
5548
|
init_();
|
|
5460
5549
|
init_2();
|
|
@@ -6057,9 +6146,10 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
6057
6146
|
return tagsByCard;
|
|
6058
6147
|
}
|
|
6059
6148
|
async getAllCardIds() {
|
|
6149
|
+
const prefix = `${DocTypePrefixes["CARD" /* CARD */]}-`;
|
|
6060
6150
|
const result = await this.db.allDocs({
|
|
6061
|
-
startkey:
|
|
6062
|
-
endkey:
|
|
6151
|
+
startkey: prefix,
|
|
6152
|
+
endkey: `${prefix}\uFFF0`,
|
|
6063
6153
|
include_docs: false
|
|
6064
6154
|
});
|
|
6065
6155
|
return result.rows.map((row) => row.id);
|
|
@@ -10477,6 +10567,7 @@ __export(index_exports, {
|
|
|
10477
10567
|
areQuestionRecords: () => areQuestionRecords,
|
|
10478
10568
|
buildStrategyStateId: () => buildStrategyStateId,
|
|
10479
10569
|
captureMixerRun: () => captureMixerRun,
|
|
10570
|
+
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
10480
10571
|
computeDeviation: () => computeDeviation,
|
|
10481
10572
|
computeEffectiveWeight: () => computeEffectiveWeight,
|
|
10482
10573
|
computeOutcomeSignal: () => computeOutcomeSignal,
|
|
@@ -10497,6 +10588,7 @@ __export(index_exports, {
|
|
|
10497
10588
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
10498
10589
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
10499
10590
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
10591
|
+
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
10500
10592
|
getStudySource: () => getStudySource,
|
|
10501
10593
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
10502
10594
|
importParsedCards: () => importParsedCards,
|
|
@@ -13444,6 +13536,7 @@ mountMixerDebugger();
|
|
|
13444
13536
|
// src/study/SessionDebugger.ts
|
|
13445
13537
|
init_logger();
|
|
13446
13538
|
init_PipelineDebugger();
|
|
13539
|
+
init_SrsDebugger();
|
|
13447
13540
|
|
|
13448
13541
|
// src/study/SessionOverlay.ts
|
|
13449
13542
|
init_logger();
|
|
@@ -13465,8 +13558,7 @@ var lastSnapshot = null;
|
|
|
13465
13558
|
var copyFlashUntil = 0;
|
|
13466
13559
|
var minified = false;
|
|
13467
13560
|
var expanded = {
|
|
13468
|
-
|
|
13469
|
-
newQ: false,
|
|
13561
|
+
supplyQ: false,
|
|
13470
13562
|
failedQ: false,
|
|
13471
13563
|
drawn: false
|
|
13472
13564
|
};
|
|
@@ -13535,7 +13627,7 @@ function render() {
|
|
|
13535
13627
|
attachHandlers();
|
|
13536
13628
|
return;
|
|
13537
13629
|
}
|
|
13538
|
-
overlayEl.innerHTML = headerHtml() + replanHtml(s) + metaHtml(s) + hintsHtml(s.sessionHints) +
|
|
13630
|
+
overlayEl.innerHTML = headerHtml() + replanHtml(s) + metaHtml(s) + hintsHtml(s.sessionHints) + backlogHtml(s.reviewBacklog) + queueHtml("supplyQ", "supplyQ", s.supplyQ) + queueHtml("failedQ", "failedQ", s.failedQ) + drawnHtml("drawn", s.drawnCards);
|
|
13539
13631
|
attachHandlers();
|
|
13540
13632
|
}
|
|
13541
13633
|
function attachHandlers() {
|
|
@@ -13627,6 +13719,29 @@ function hintsHtml(h) {
|
|
|
13627
13719
|
const body = parts.length ? parts.map((p) => `<div style="margin-left:6px">${p}</div>`).join("") : `<div style="margin-left:6px;opacity:.6">none</div>`;
|
|
13628
13720
|
return `<div style="margin-bottom:6px"><div style="color:#86efac">sessionHints</div>${body}</div>`;
|
|
13629
13721
|
}
|
|
13722
|
+
function backlogHtml(backlog) {
|
|
13723
|
+
if (!backlog.length) return "";
|
|
13724
|
+
const rows = backlog.map((b) => {
|
|
13725
|
+
const maxed = b.backlogMultiplier >= b.maxBacklogMultiplier - 1e-9;
|
|
13726
|
+
const multColor = b.backlogMultiplier <= 1 ? "#86efac" : maxed ? "#fca5a5" : "#fcd34d";
|
|
13727
|
+
const headroom = maxed ? "maxed \u2014 boosts decide order until they relax" : b.backlogMultiplier > 1 ? "climbing as backlog grows" : "healthy \u2014 no pressure";
|
|
13728
|
+
const top = b.topReviewScore !== null ? b.topReviewScore.toFixed(2) : "\u2014";
|
|
13729
|
+
const next = b.nextDueIn ? ` <span style="opacity:.6">\xB7 next due ${esc(b.nextDueIn)}</span>` : "";
|
|
13730
|
+
return `<div style="margin-left:6px"><span style="opacity:.7">${esc(b.courseId.slice(0, 8))}</span> due ${b.dueNow}/${b.scheduledTotal} <span style="opacity:.6">(healthy ${b.healthyBacklog})</span>${next}<div style="margin-left:6px">pressure <span style="color:${multColor}">\xD7${b.backlogMultiplier.toFixed(2)}/${b.maxBacklogMultiplier.toFixed(2)}</span> <span style="opacity:.6">${headroom} \xB7 top review ${top}</span></div></div>`;
|
|
13731
|
+
}).join("");
|
|
13732
|
+
return `<div style="margin-bottom:6px"><div style="color:#93c5fd">review backpressure</div>${rows}</div>`;
|
|
13733
|
+
}
|
|
13734
|
+
function fmtScore(score) {
|
|
13735
|
+
if (score === void 0) return "";
|
|
13736
|
+
if (!Number.isFinite(score)) return "REQ";
|
|
13737
|
+
return score.toFixed(2);
|
|
13738
|
+
}
|
|
13739
|
+
function queueItemHtml(item) {
|
|
13740
|
+
const tagColor = item.origin === "review" ? "#93c5fd" : "#fcd34d";
|
|
13741
|
+
const score = fmtScore(item.score);
|
|
13742
|
+
const label = `${item.origin === "review" ? "r" : "n"}${score ? " " + score : ""}`;
|
|
13743
|
+
return `${esc(item.cardID)}<span style="color:${tagColor};opacity:.85"> [${label}]</span>`;
|
|
13744
|
+
}
|
|
13630
13745
|
function queueHtml(key, label, q) {
|
|
13631
13746
|
const collapsible = q.length > INLINE_THRESHOLD;
|
|
13632
13747
|
const isOpen = collapsible && expanded[key];
|
|
@@ -13641,7 +13756,7 @@ function queueHtml(key, label, q) {
|
|
|
13641
13756
|
const shown = isOpen ? q.cards : q.cards.slice(0, INLINE_THRESHOLD);
|
|
13642
13757
|
const hiddenCount = q.length - shown.length;
|
|
13643
13758
|
const listMarginBottom = collapsible ? 2 : 6;
|
|
13644
|
-
let body = `<ol style="margin:2px 0 ${listMarginBottom}px 0;padding-left:20px">` + shown.map((c) => `<li style="white-space:nowrap">${
|
|
13759
|
+
let body = `<ol style="margin:2px 0 ${listMarginBottom}px 0;padding-left:20px">` + shown.map((c) => `<li style="white-space:nowrap">${queueItemHtml(c)}</li>`).join("") + `</ol>`;
|
|
13645
13760
|
if (collapsible) {
|
|
13646
13761
|
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13647
13762
|
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
@@ -13704,13 +13819,29 @@ function snapshotToText(s) {
|
|
|
13704
13819
|
if (h.excludeCards?.length) hintParts.push(` excludeCards: ${h.excludeCards.join(", ")}`);
|
|
13705
13820
|
}
|
|
13706
13821
|
lines.push(hintParts.length ? hintParts.join("\n") : " none");
|
|
13822
|
+
if (s.reviewBacklog.length) {
|
|
13823
|
+
lines.push("");
|
|
13824
|
+
lines.push("review backpressure:");
|
|
13825
|
+
for (const b of s.reviewBacklog) {
|
|
13826
|
+
const maxed = b.backlogMultiplier >= b.maxBacklogMultiplier - 1e-9;
|
|
13827
|
+
const headroom = maxed ? "maxed (boosts decide order)" : b.backlogMultiplier > 1 ? "climbing" : "healthy";
|
|
13828
|
+
const top = b.topReviewScore !== null ? b.topReviewScore.toFixed(2) : "\u2014";
|
|
13829
|
+
const next = b.nextDueIn ? `, next due ${b.nextDueIn}` : "";
|
|
13830
|
+
lines.push(
|
|
13831
|
+
` ${b.courseId.slice(0, 8)}: due ${b.dueNow}/${b.scheduledTotal} (healthy ${b.healthyBacklog})${next}; pressure \xD7${b.backlogMultiplier.toFixed(2)}/${b.maxBacklogMultiplier.toFixed(2)} ${headroom}; top review ${top}`
|
|
13832
|
+
);
|
|
13833
|
+
}
|
|
13834
|
+
}
|
|
13707
13835
|
const queueText = (label, q) => {
|
|
13708
13836
|
lines.push("");
|
|
13709
13837
|
lines.push(`${label}: ${q.length} (drawn ${q.dequeueCount})`);
|
|
13710
|
-
q.cards.forEach((c, i) =>
|
|
13838
|
+
q.cards.forEach((c, i) => {
|
|
13839
|
+
const score = fmtScore(c.score);
|
|
13840
|
+
const tag = `${c.origin === "review" ? "r" : "n"}${score ? " " + score : ""}`;
|
|
13841
|
+
lines.push(` ${i + 1}. ${c.cardID} [${tag}]`);
|
|
13842
|
+
});
|
|
13711
13843
|
};
|
|
13712
|
-
queueText("
|
|
13713
|
-
queueText("newQ", s.newQ);
|
|
13844
|
+
queueText("supplyQ", s.supplyQ);
|
|
13714
13845
|
queueText("failedQ", s.failedQ);
|
|
13715
13846
|
lines.push("");
|
|
13716
13847
|
lines.push(`drawn: ${s.drawnCards.length}`);
|
|
@@ -13735,16 +13866,16 @@ function esc(value) {
|
|
|
13735
13866
|
var activeSession = null;
|
|
13736
13867
|
var sessionHistory = [];
|
|
13737
13868
|
var MAX_HISTORY = 5;
|
|
13738
|
-
function startSessionTracking(
|
|
13869
|
+
function startSessionTracking(supplyQLength, failedQLength) {
|
|
13739
13870
|
clearRunHistory();
|
|
13871
|
+
clearSrsBacklogDebug();
|
|
13740
13872
|
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
13741
13873
|
activeSession = {
|
|
13742
13874
|
sessionId,
|
|
13743
13875
|
startTime: /* @__PURE__ */ new Date(),
|
|
13744
13876
|
initialQueues: {
|
|
13745
13877
|
timestamp: /* @__PURE__ */ new Date(),
|
|
13746
|
-
|
|
13747
|
-
newQLength,
|
|
13878
|
+
supplyQLength,
|
|
13748
13879
|
failedQLength
|
|
13749
13880
|
},
|
|
13750
13881
|
presentations: [],
|
|
@@ -13768,17 +13899,15 @@ function recordCardPresentation(cardId, courseId, courseName, origin, queueSourc
|
|
|
13768
13899
|
score
|
|
13769
13900
|
});
|
|
13770
13901
|
}
|
|
13771
|
-
function snapshotQueues(
|
|
13902
|
+
function snapshotQueues(supplyQLength, failedQLength, supplyQNext3) {
|
|
13772
13903
|
if (!activeSession) {
|
|
13773
13904
|
return;
|
|
13774
13905
|
}
|
|
13775
13906
|
activeSession.queueSnapshots.push({
|
|
13776
13907
|
timestamp: /* @__PURE__ */ new Date(),
|
|
13777
|
-
|
|
13778
|
-
newQLength,
|
|
13908
|
+
supplyQLength,
|
|
13779
13909
|
failedQLength,
|
|
13780
|
-
|
|
13781
|
-
newQNext3
|
|
13910
|
+
supplyQNext3
|
|
13782
13911
|
});
|
|
13783
13912
|
}
|
|
13784
13913
|
function endSessionTracking() {
|
|
@@ -13800,13 +13929,9 @@ function showCurrentQueue() {
|
|
|
13800
13929
|
}
|
|
13801
13930
|
const latest = activeSession.queueSnapshots[activeSession.queueSnapshots.length - 1] || activeSession.initialQueues;
|
|
13802
13931
|
console.group("\u{1F4CA} Current Queue State");
|
|
13803
|
-
logger.info(`
|
|
13804
|
-
if (latest.
|
|
13805
|
-
logger.info(` Next: ${latest.
|
|
13806
|
-
}
|
|
13807
|
-
logger.info(`New Queue: ${latest.newQLength} cards`);
|
|
13808
|
-
if (latest.newQNext3 && latest.newQNext3.length > 0) {
|
|
13809
|
-
logger.info(` Next: ${latest.newQNext3.join(", ")}`);
|
|
13932
|
+
logger.info(`Supply Queue: ${latest.supplyQLength} cards`);
|
|
13933
|
+
if (latest.supplyQNext3 && latest.supplyQNext3.length > 0) {
|
|
13934
|
+
logger.info(` Next: ${latest.supplyQNext3.join(", ")}`);
|
|
13810
13935
|
}
|
|
13811
13936
|
logger.info(`Failed Queue: ${latest.failedQLength} cards`);
|
|
13812
13937
|
console.groupEnd();
|
|
@@ -14023,15 +14148,6 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14023
14148
|
* Individual replans can override via `ReplanOptions.limit`.
|
|
14024
14149
|
*/
|
|
14025
14150
|
_defaultBatchLimit = 20;
|
|
14026
|
-
/**
|
|
14027
|
-
* Maximum number of reviews enqueued at session start. Reviews live
|
|
14028
|
-
* outside the replan flow — the queue drains via consumption and is
|
|
14029
|
-
* not refilled mid-session. The session timer caps total review
|
|
14030
|
-
* exposure, so overfilling here is intentional. Default is generous
|
|
14031
|
-
* to accommodate Anki-style power users with hundreds of due reviews;
|
|
14032
|
-
* apps targeting nimbler sessions should override via constructor.
|
|
14033
|
-
*/
|
|
14034
|
-
_initialReviewCap = 200;
|
|
14035
14151
|
sources;
|
|
14036
14152
|
// dataLayer and getViewComponent now injected into CardHydrationService
|
|
14037
14153
|
_sessionRecord = [];
|
|
@@ -14040,10 +14156,28 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14040
14156
|
}
|
|
14041
14157
|
// Session card stores
|
|
14042
14158
|
_currentCard = null;
|
|
14043
|
-
|
|
14044
|
-
|
|
14159
|
+
/**
|
|
14160
|
+
* The single supply queue: `new` + `review` items interleaved in pipeline
|
|
14161
|
+
* rank order (the mixer's score-ordered, source-interleaved output, with
|
|
14162
|
+
* `+INF` required cards floated to the front). Drawn front-to-back; reviews
|
|
14163
|
+
* and new compete on one cross-comparable scale rather than being re-mixed
|
|
14164
|
+
* by a probability gate. Replaced/re-ranked wholesale on replan. See
|
|
14165
|
+
* `docs/decision-single-supply-queue.md`.
|
|
14166
|
+
*/
|
|
14167
|
+
supplyQ = new ItemQueue();
|
|
14045
14168
|
failedQ = new ItemQueue();
|
|
14046
14169
|
// END Session card stores
|
|
14170
|
+
/**
|
|
14171
|
+
* Supply draws since the last failed-queue *event* (a failed draw, or a card
|
|
14172
|
+
* entering failedQ on failure). Drives the light steady failed-interleave
|
|
14173
|
+
* (§7): after this many consecutive supply draws, a pending failed card is
|
|
14174
|
+
* drawn so remediation doesn't starve mid-session. Incremented on each supply
|
|
14175
|
+
* draw; reset to 0 both when a failed card is drawn AND when one is added to
|
|
14176
|
+
* failedQ — the latter gives a just-failed card spacing instead of an instant
|
|
14177
|
+
* retry (the counter would otherwise already be ≥ threshold from the preceding
|
|
14178
|
+
* supply run).
|
|
14179
|
+
*/
|
|
14180
|
+
_supplyDrawsSinceFailed = 0;
|
|
14047
14181
|
/**
|
|
14048
14182
|
* Promise tracking a currently in-progress replan, or null if idle.
|
|
14049
14183
|
* Used by nextCard() to await completion before drawing from queues.
|
|
@@ -14057,8 +14191,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14057
14191
|
*/
|
|
14058
14192
|
_activeReplanLabel = null;
|
|
14059
14193
|
/**
|
|
14060
|
-
* Number of well-indicated
|
|
14061
|
-
* degrades to poorly-indicated content. Decremented on each
|
|
14194
|
+
* Number of well-indicated supply cards remaining before the queue
|
|
14195
|
+
* degrades to poorly-indicated content. Decremented on each supplyQ
|
|
14062
14196
|
* draw; when it hits 0, a replan is triggered automatically
|
|
14063
14197
|
* (user state has changed from completing good cards).
|
|
14064
14198
|
*/
|
|
@@ -14067,7 +14201,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14067
14201
|
* When true, suppresses the quality-based auto-replan trigger in
|
|
14068
14202
|
* nextCard(). Set after a burst replan (small limit) to prevent the
|
|
14069
14203
|
* auto-replan from clobbering the burst cards before they're consumed.
|
|
14070
|
-
* Cleared when the depletion-triggered replan fires (
|
|
14204
|
+
* Cleared when the depletion-triggered replan fires (supplyQ exhausted).
|
|
14071
14205
|
*/
|
|
14072
14206
|
_suppressQualityReplan = false;
|
|
14073
14207
|
/**
|
|
@@ -14096,13 +14230,15 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14096
14230
|
* a draw the instant it happens — earlier than `_sessionRecord`, which only
|
|
14097
14231
|
* lands once the card is *responded to*.
|
|
14098
14232
|
*
|
|
14099
|
-
* Used to keep already-served cards out of
|
|
14100
|
-
* card shown once must never re-enter
|
|
14101
|
-
*
|
|
14102
|
-
*
|
|
14103
|
-
*
|
|
14104
|
-
*
|
|
14105
|
-
*
|
|
14233
|
+
* Used to keep already-served cards out of supplyQ on every (re)plan, across
|
|
14234
|
+
* ALL origins: a `new` card shown once must never re-enter, and once replans
|
|
14235
|
+
* re-pull reviews, an answered/in-flight review must not re-enter the supply
|
|
14236
|
+
* before its SRS reschedule clears the due-window (the review-loop guard,
|
|
14237
|
+
* decision doc §4). This is the general guard against re-presentation —
|
|
14238
|
+
* including the case where a replan in flight captured a now-drawn card (e.g.
|
|
14239
|
+
* a +INF require-injected follow-up the depletion prefetch grabbed just before
|
|
14240
|
+
* it was drawn). failedQ is separate and controller-owned, so failed cards
|
|
14241
|
+
* legitimately recur there without being gated here.
|
|
14106
14242
|
*/
|
|
14107
14243
|
_servedCardIds = /* @__PURE__ */ new Set();
|
|
14108
14244
|
/**
|
|
@@ -14127,14 +14263,12 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14127
14263
|
return this._minCardsGuarantee > 0;
|
|
14128
14264
|
}
|
|
14129
14265
|
get report() {
|
|
14130
|
-
const
|
|
14131
|
-
const
|
|
14132
|
-
|
|
14133
|
-
const newCardWord = newCount === 1 ? "new card" : "new cards";
|
|
14134
|
-
return `${reviewCount} ${reviewWord}, ${newCount} ${newCardWord}`;
|
|
14266
|
+
const supplyCount = this.supplyQ.dequeueCount;
|
|
14267
|
+
const supplyWord = supplyCount === 1 ? "card" : "cards";
|
|
14268
|
+
return `${supplyCount} supply ${supplyWord} drawn`;
|
|
14135
14269
|
}
|
|
14136
14270
|
get detailedReport() {
|
|
14137
|
-
return this.
|
|
14271
|
+
return this.supplyQ.toString + "\n" + this.failedQ.toString;
|
|
14138
14272
|
}
|
|
14139
14273
|
// @ts-expect-error NodeJS.Timeout type not available in browser context
|
|
14140
14274
|
_intervalHandle;
|
|
@@ -14145,11 +14279,9 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14145
14279
|
* @param getViewComponent - Function to resolve view components
|
|
14146
14280
|
* @param mixer - Optional source mixer strategy (defaults to QuotaRoundRobinMixer)
|
|
14147
14281
|
* @param options - Optional session-level configuration
|
|
14148
|
-
* @param options.defaultBatchLimit - Default
|
|
14282
|
+
* @param options.defaultBatchLimit - Default supply working-set size (default: 20).
|
|
14149
14283
|
* Smaller values for newer users cause more frequent replans, keeping plans
|
|
14150
14284
|
* aligned with rapidly-changing user state.
|
|
14151
|
-
* @param options.initialReviewCap - Max reviews loaded at session start (default: 200).
|
|
14152
|
-
* Applied only on initial planning; replans do not refill the review queue.
|
|
14153
14285
|
*/
|
|
14154
14286
|
constructor(sources, time, dataLayer, getViewComponent, mixer, options) {
|
|
14155
14287
|
super();
|
|
@@ -14172,17 +14304,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14172
14304
|
if (options?.defaultBatchLimit !== void 0) {
|
|
14173
14305
|
this._defaultBatchLimit = options.defaultBatchLimit;
|
|
14174
14306
|
}
|
|
14175
|
-
if (options?.initialReviewCap !== void 0) {
|
|
14176
|
-
this._initialReviewCap = options.initialReviewCap;
|
|
14177
|
-
}
|
|
14178
14307
|
if (options?.outcomeObservers?.length) {
|
|
14179
14308
|
this._outcomeObservers = [...options.outcomeObservers];
|
|
14180
14309
|
}
|
|
14181
14310
|
this.log(`Session constructed:
|
|
14182
14311
|
startTime: ${this.startTime}
|
|
14183
14312
|
endTime: ${this.endTime}
|
|
14184
|
-
defaultBatchLimit: ${this._defaultBatchLimit}
|
|
14185
|
-
initialReviewCap: ${this._initialReviewCap}`);
|
|
14313
|
+
defaultBatchLimit: ${this._defaultBatchLimit}`);
|
|
14186
14314
|
registerActiveController(this);
|
|
14187
14315
|
}
|
|
14188
14316
|
tick() {
|
|
@@ -14216,15 +14344,6 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14216
14344
|
this.log(`Failed card cleanup estimate: ${Math.round(ret)}`);
|
|
14217
14345
|
return ret;
|
|
14218
14346
|
}
|
|
14219
|
-
/**
|
|
14220
|
-
* Extremely rough, conservative, estimate of amound of time to complete
|
|
14221
|
-
* all scheduled reviews
|
|
14222
|
-
*/
|
|
14223
|
-
estimateReviewTime() {
|
|
14224
|
-
const ret = 5 * this.reviewQ.length;
|
|
14225
|
-
this.log(`Review card time estimate: ${ret}`);
|
|
14226
|
-
return ret;
|
|
14227
|
-
}
|
|
14228
14347
|
async prepareSession() {
|
|
14229
14348
|
if (this.sources.some((s) => typeof s.getWeightedCards !== "function")) {
|
|
14230
14349
|
throw new Error(
|
|
@@ -14239,15 +14358,15 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14239
14358
|
);
|
|
14240
14359
|
}
|
|
14241
14360
|
await this.hydrationService.ensureHydratedCards();
|
|
14242
|
-
startSessionTracking(this.
|
|
14361
|
+
startSessionTracking(this.supplyQ.length, this.failedQ.length);
|
|
14243
14362
|
this._intervalHandle = setInterval(() => {
|
|
14244
14363
|
this.tick();
|
|
14245
14364
|
}, 1e3);
|
|
14246
14365
|
}
|
|
14247
14366
|
/**
|
|
14248
14367
|
* Request a mid-session replan. Re-runs the pipeline with current user state
|
|
14249
|
-
* and atomically replaces the
|
|
14250
|
-
* a session.
|
|
14368
|
+
* and atomically replaces (or merges into) the supplyQ contents. Safe to call
|
|
14369
|
+
* at any time during a session.
|
|
14251
14370
|
*
|
|
14252
14371
|
* Concurrency policy:
|
|
14253
14372
|
* - Two unhinted auto-replans never run in parallel; the second coalesces
|
|
@@ -14261,7 +14380,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14261
14380
|
* results (e.g. surfacing another gpc-intro card right after one
|
|
14262
14381
|
* completed, skipping the prescribed `c-wst-*` follow-up).
|
|
14263
14382
|
*
|
|
14264
|
-
*
|
|
14383
|
+
* Re-pulls and re-ranks the whole supply (including reviews); does NOT affect
|
|
14384
|
+
* failedQ (controller-owned remediation).
|
|
14265
14385
|
*
|
|
14266
14386
|
* If nextCard() is called while a replan is in flight, it will automatically
|
|
14267
14387
|
* await the replan before drawing from queues, ensuring the user always sees
|
|
@@ -14327,7 +14447,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14327
14447
|
* excludeCards happen at *invocation* time, not at *queue* time. For a
|
|
14328
14448
|
* queued replan that means excludes reflect the state after the prior
|
|
14329
14449
|
* replan landed — which is what we want, since the prior replan's
|
|
14330
|
-
*
|
|
14450
|
+
* supplyQ.peek(0) is the imminent draw we need to exclude.
|
|
14331
14451
|
*/
|
|
14332
14452
|
async _runReplan(opts) {
|
|
14333
14453
|
this._activeReplanLabel = opts.label ?? "(auto)";
|
|
@@ -14340,8 +14460,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14340
14460
|
for (const rec of this._sessionRecord) {
|
|
14341
14461
|
excludeSet.add(rec.card.card_id);
|
|
14342
14462
|
}
|
|
14343
|
-
if (this.
|
|
14344
|
-
excludeSet.add(this.
|
|
14463
|
+
if (this.supplyQ.length > 0) {
|
|
14464
|
+
excludeSet.add(this.supplyQ.peek(0).cardID);
|
|
14345
14465
|
}
|
|
14346
14466
|
hints.excludeCards = [...excludeSet];
|
|
14347
14467
|
if (opts.sessionHints !== void 0) {
|
|
@@ -14402,7 +14522,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14402
14522
|
const describe = (q) => {
|
|
14403
14523
|
const cards = [];
|
|
14404
14524
|
for (let i = 0; i < q.length; i++) {
|
|
14405
|
-
|
|
14525
|
+
const item = q.peek(i);
|
|
14526
|
+
cards.push({
|
|
14527
|
+
cardID: item.cardID,
|
|
14528
|
+
status: item.status,
|
|
14529
|
+
origin: isReview(item) ? "review" : "new",
|
|
14530
|
+
score: item.score
|
|
14531
|
+
});
|
|
14406
14532
|
}
|
|
14407
14533
|
return { length: q.length, dequeueCount: q.dequeueCount, cards };
|
|
14408
14534
|
};
|
|
@@ -14425,9 +14551,9 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14425
14551
|
sessionHints: this._sessionHints,
|
|
14426
14552
|
replanActive: this._replanPromise !== null,
|
|
14427
14553
|
replanLabel: this._activeReplanLabel,
|
|
14428
|
-
|
|
14429
|
-
newQ: describe(this.newQ),
|
|
14554
|
+
supplyQ: describe(this.supplyQ),
|
|
14430
14555
|
failedQ: describe(this.failedQ),
|
|
14556
|
+
reviewBacklog: getSrsBacklogDebug(),
|
|
14431
14557
|
drawnCards
|
|
14432
14558
|
};
|
|
14433
14559
|
}
|
|
@@ -14547,7 +14673,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14547
14673
|
*/
|
|
14548
14674
|
static WELL_INDICATED_SCORE = 0.1;
|
|
14549
14675
|
/**
|
|
14550
|
-
*
|
|
14676
|
+
* supplyQ length at or below which the opportunistic depletion-prefetch
|
|
14551
14677
|
* fires. Sets the lead time available for the background replan to land
|
|
14552
14678
|
* before the user actually empties the queue and falls into the
|
|
14553
14679
|
* (synchronous) wedge-breaker path.
|
|
@@ -14560,7 +14686,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14560
14686
|
*/
|
|
14561
14687
|
static DEPLETION_PREFETCH_THRESHOLD = 3;
|
|
14562
14688
|
/**
|
|
14563
|
-
* Internal replan execution. Runs the pipeline,
|
|
14689
|
+
* Internal replan execution. Runs the pipeline, rebuilds the supplyQ,
|
|
14564
14690
|
* atomically swaps it in, and triggers hydration for the new contents.
|
|
14565
14691
|
*
|
|
14566
14692
|
* If the initial replan produces fewer than MIN_WELL_INDICATED cards that
|
|
@@ -14589,8 +14715,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14589
14715
|
}
|
|
14590
14716
|
await this.hydrationService.ensureHydratedCards();
|
|
14591
14717
|
const labelTag = opts.label ? ` [${opts.label}]` : "";
|
|
14592
|
-
this.log(`Replan complete${labelTag}:
|
|
14593
|
-
snapshotQueues(this.
|
|
14718
|
+
this.log(`Replan complete${labelTag}: supplyQ now has ${this.supplyQ.length} cards (mode=${mode})`);
|
|
14719
|
+
snapshotQueues(this.supplyQ.length, this.failedQ.length);
|
|
14594
14720
|
}
|
|
14595
14721
|
addTime(seconds) {
|
|
14596
14722
|
this.endTime = new Date(this.endTime.valueOf() + 1e3 * seconds);
|
|
@@ -14599,10 +14725,10 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14599
14725
|
return this.failedQ.length;
|
|
14600
14726
|
}
|
|
14601
14727
|
toString() {
|
|
14602
|
-
return `Session: ${this.
|
|
14728
|
+
return `Session: ${this.supplyQ.length} supply, ${this.failedQ.length} failed`;
|
|
14603
14729
|
}
|
|
14604
14730
|
reportString() {
|
|
14605
|
-
return `${this.
|
|
14731
|
+
return `${this.supplyQ.dequeueCount} supply, ${this.failedQ.dequeueCount} failed`;
|
|
14606
14732
|
}
|
|
14607
14733
|
/**
|
|
14608
14734
|
* Returns debug information about the current session state.
|
|
@@ -14619,7 +14745,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14619
14745
|
items.push({
|
|
14620
14746
|
courseID: item.courseID || "unknown",
|
|
14621
14747
|
cardID: item.cardID || "unknown",
|
|
14622
|
-
status: item.status || "unknown"
|
|
14748
|
+
status: item.status || "unknown",
|
|
14749
|
+
score: item.score
|
|
14623
14750
|
});
|
|
14624
14751
|
}
|
|
14625
14752
|
return items;
|
|
@@ -14629,15 +14756,10 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14629
14756
|
mode: supportsWeightedCards ? "weighted" : "legacy",
|
|
14630
14757
|
description: supportsWeightedCards ? "Using getWeightedCards() API with scored candidates" : "ERROR: getWeightedCards() not a function."
|
|
14631
14758
|
},
|
|
14632
|
-
|
|
14633
|
-
length: this.
|
|
14634
|
-
dequeueCount: this.
|
|
14635
|
-
items: extractQueueItems(this.
|
|
14636
|
-
},
|
|
14637
|
-
newQueue: {
|
|
14638
|
-
length: this.newQ.length,
|
|
14639
|
-
dequeueCount: this.newQ.dequeueCount,
|
|
14640
|
-
items: extractQueueItems(this.newQ)
|
|
14759
|
+
supplyQueue: {
|
|
14760
|
+
length: this.supplyQ.length,
|
|
14761
|
+
dequeueCount: this.supplyQ.dequeueCount,
|
|
14762
|
+
items: extractQueueItems(this.supplyQ)
|
|
14641
14763
|
},
|
|
14642
14764
|
failedQueue: {
|
|
14643
14765
|
length: this.failedQ.length,
|
|
@@ -14657,30 +14779,29 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14657
14779
|
};
|
|
14658
14780
|
}
|
|
14659
14781
|
/**
|
|
14660
|
-
* Fetch content
|
|
14782
|
+
* Fetch weighted content from all sources, mix across sources, and populate
|
|
14783
|
+
* the single supply queue in pipeline rank order.
|
|
14661
14784
|
*
|
|
14662
|
-
*
|
|
14663
|
-
* 1.
|
|
14664
|
-
*
|
|
14665
|
-
*
|
|
14666
|
-
*
|
|
14667
|
-
|
|
14668
|
-
/**
|
|
14669
|
-
* Fetch weighted content from all sources and populate session queues.
|
|
14785
|
+
* Reviews and new cards compete on one cross-comparable scale (SRS 0.5–1.0
|
|
14786
|
+
* w/ backlog pressure vs ELO 0.0–1.0) — there is no origin split and no
|
|
14787
|
+
* second mixer. The working set is `supplyLimit` cards (the top of the mixed
|
|
14788
|
+
* ranking, plus any `+INF` required cards floated to the front); replans
|
|
14789
|
+
* re-pull and re-rank the whole supply, so a heavy review backlog surfaces as
|
|
14790
|
+
* a refreshed top-ranked working set rather than a frozen 200-card snapshot.
|
|
14670
14791
|
*
|
|
14671
14792
|
* @param options.replan - If true, this is a mid-session replan rather than
|
|
14672
|
-
* initial session setup.
|
|
14673
|
-
*
|
|
14674
|
-
* @param options.additive - If true (replan only), merge
|
|
14675
|
-
* candidates into the front of the existing
|
|
14793
|
+
* initial session setup. Atomically replaces supplyQ contents and treats
|
|
14794
|
+
* empty results as non-fatal.
|
|
14795
|
+
* @param options.additive - If true (replan only), merge high-quality
|
|
14796
|
+
* candidates into the front of the existing supplyQ instead of replacing it.
|
|
14676
14797
|
* @returns Number of "well-indicated" cards (passed all hierarchy filters)
|
|
14677
14798
|
* in the new content. Returns -1 if no content was loaded.
|
|
14678
14799
|
*/
|
|
14679
14800
|
async getWeightedContent(options) {
|
|
14680
14801
|
const replan = options?.replan ?? false;
|
|
14681
14802
|
const additive = options?.additive ?? false;
|
|
14682
|
-
const
|
|
14683
|
-
const fetchLimit =
|
|
14803
|
+
const supplyLimit = options?.limit ?? this._defaultBatchLimit;
|
|
14804
|
+
const fetchLimit = supplyLimit;
|
|
14684
14805
|
if (!replan) {
|
|
14685
14806
|
this._applyHintsToSources();
|
|
14686
14807
|
}
|
|
@@ -14702,7 +14823,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14702
14823
|
}
|
|
14703
14824
|
if (batches.length === 0) {
|
|
14704
14825
|
if (replan) {
|
|
14705
|
-
this.log("Replan: no content from any source, keeping existing
|
|
14826
|
+
this.log("Replan: no content from any source, keeping existing supplyQ");
|
|
14706
14827
|
return -1;
|
|
14707
14828
|
}
|
|
14708
14829
|
throw new Error(
|
|
@@ -14736,64 +14857,59 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14736
14857
|
quotaPerSource,
|
|
14737
14858
|
mixedWeighted
|
|
14738
14859
|
);
|
|
14739
|
-
const
|
|
14740
|
-
const
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
const mandatoryWeighted = newCandidates.filter((w) => w.score === Number.POSITIVE_INFINITY);
|
|
14744
|
-
const optionalWeighted = newCandidates.filter((w) => w.score !== Number.POSITIVE_INFINITY);
|
|
14745
|
-
const newWeighted = [
|
|
14860
|
+
const candidates = mixedWeighted.filter((w) => !this._servedCardIds.has(w.cardId));
|
|
14861
|
+
const mandatoryWeighted = candidates.filter((w) => w.score === Number.POSITIVE_INFINITY);
|
|
14862
|
+
const optionalWeighted = candidates.filter((w) => w.score !== Number.POSITIVE_INFINITY);
|
|
14863
|
+
const supplyWeighted = [
|
|
14746
14864
|
...mandatoryWeighted,
|
|
14747
|
-
...optionalWeighted.slice(0, Math.max(0,
|
|
14865
|
+
...optionalWeighted.slice(0, Math.max(0, supplyLimit - mandatoryWeighted.length))
|
|
14748
14866
|
];
|
|
14749
14867
|
const mandatoryIds = new Set(mandatoryWeighted.map((w) => w.cardId));
|
|
14750
|
-
|
|
14751
|
-
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
14752
|
-
if (!replan) {
|
|
14753
|
-
for (const w of reviewWeighted) {
|
|
14754
|
-
const reviewItem = {
|
|
14755
|
-
cardID: w.cardId,
|
|
14756
|
-
courseID: w.courseId,
|
|
14757
|
-
contentSourceType: "course",
|
|
14758
|
-
contentSourceID: w.courseId,
|
|
14759
|
-
reviewID: w.reviewID,
|
|
14760
|
-
status: "review"
|
|
14761
|
-
};
|
|
14762
|
-
this.reviewQ.add(reviewItem, reviewItem.cardID);
|
|
14763
|
-
report += `Review: ${w.courseId}::${w.cardId} (score: ${w.score.toFixed(2)})
|
|
14764
|
-
`;
|
|
14765
|
-
}
|
|
14766
|
-
}
|
|
14767
|
-
const wellIndicated = newWeighted.filter(
|
|
14868
|
+
const wellIndicated = supplyWeighted.filter(
|
|
14768
14869
|
(w) => w.score >= _SessionController.WELL_INDICATED_SCORE
|
|
14769
14870
|
).length;
|
|
14770
|
-
|
|
14771
|
-
|
|
14772
|
-
const
|
|
14773
|
-
|
|
14774
|
-
|
|
14775
|
-
contentSourceType: "course",
|
|
14776
|
-
contentSourceID: w.courseId,
|
|
14777
|
-
status: "new"
|
|
14778
|
-
};
|
|
14779
|
-
newItems.push(newItem);
|
|
14780
|
-
report += `New: ${w.courseId}::${w.cardId} (score: ${w.score.toFixed(2)})
|
|
14871
|
+
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
14872
|
+
const supplyItems = supplyWeighted.map((w) => {
|
|
14873
|
+
const origin = getCardOrigin(w);
|
|
14874
|
+
const scoreStr = Number.isFinite(w.score) ? w.score.toFixed(2) : "+INF";
|
|
14875
|
+
report += `${origin === "review" ? "Review" : "New"}: ${w.courseId}::${w.cardId} (score: ${scoreStr})
|
|
14781
14876
|
`;
|
|
14782
|
-
|
|
14877
|
+
return this._buildSupplyItem(w, origin);
|
|
14878
|
+
});
|
|
14783
14879
|
if (additive) {
|
|
14784
|
-
const added = this.
|
|
14785
|
-
report += `Additive merge: ${added}
|
|
14880
|
+
const added = this.supplyQ.mergeToFront(supplyItems, (item) => item.cardID, mandatoryIds);
|
|
14881
|
+
report += `Additive merge: ${added} cards added to front of supplyQ
|
|
14786
14882
|
`;
|
|
14787
14883
|
} else if (replan) {
|
|
14788
|
-
this.
|
|
14884
|
+
this.supplyQ.replaceAll(supplyItems, (item) => item.cardID);
|
|
14789
14885
|
} else {
|
|
14790
|
-
for (const item of
|
|
14791
|
-
this.
|
|
14886
|
+
for (const item of supplyItems) {
|
|
14887
|
+
this.supplyQ.add(item, item.cardID);
|
|
14792
14888
|
}
|
|
14793
14889
|
}
|
|
14794
14890
|
this.log(report);
|
|
14795
14891
|
return wellIndicated;
|
|
14796
14892
|
}
|
|
14893
|
+
/**
|
|
14894
|
+
* Build a supply item from a weighted candidate. Review-origin cards carry
|
|
14895
|
+
* their `reviewID` so SRS outcome tracking and re-presentation work; new
|
|
14896
|
+
* cards do not. `score` is carried on both for the debug overlay.
|
|
14897
|
+
*/
|
|
14898
|
+
_buildSupplyItem(w, origin = getCardOrigin(w)) {
|
|
14899
|
+
const base = {
|
|
14900
|
+
cardID: w.cardId,
|
|
14901
|
+
courseID: w.courseId,
|
|
14902
|
+
contentSourceType: "course",
|
|
14903
|
+
contentSourceID: w.courseId,
|
|
14904
|
+
score: w.score
|
|
14905
|
+
};
|
|
14906
|
+
if (origin === "review") {
|
|
14907
|
+
const reviewItem = { ...base, status: "review", reviewID: w.reviewID };
|
|
14908
|
+
return reviewItem;
|
|
14909
|
+
}
|
|
14910
|
+
const newItem = { ...base, status: "new" };
|
|
14911
|
+
return newItem;
|
|
14912
|
+
}
|
|
14797
14913
|
/**
|
|
14798
14914
|
* Returns items that should be pre-hydrated.
|
|
14799
14915
|
* Deterministic: top N items from each queue to ensure coverage.
|
|
@@ -14801,71 +14917,73 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14801
14917
|
*/
|
|
14802
14918
|
_getItemsToHydrate() {
|
|
14803
14919
|
const items = [];
|
|
14804
|
-
const
|
|
14805
|
-
|
|
14806
|
-
|
|
14807
|
-
|
|
14808
|
-
for (let i = 0; i < Math.min(ITEMS_PER_QUEUE, this.newQ.length); i++) {
|
|
14809
|
-
items.push(this.newQ.peek(i));
|
|
14920
|
+
const SUPPLY_PREFETCH = 3;
|
|
14921
|
+
const FAILED_PREFETCH = 2;
|
|
14922
|
+
for (let i = 0; i < Math.min(SUPPLY_PREFETCH, this.supplyQ.length); i++) {
|
|
14923
|
+
items.push(this.supplyQ.peek(i));
|
|
14810
14924
|
}
|
|
14811
|
-
for (let i = 0; i < Math.min(
|
|
14925
|
+
for (let i = 0; i < Math.min(FAILED_PREFETCH, this.failedQ.length); i++) {
|
|
14812
14926
|
items.push(this.failedQ.peek(i));
|
|
14813
14927
|
}
|
|
14814
14928
|
return items;
|
|
14815
14929
|
}
|
|
14816
14930
|
/**
|
|
14817
14931
|
* Selects the next item to present to the user.
|
|
14818
|
-
*
|
|
14932
|
+
*
|
|
14933
|
+
* The supplyQ is already rank-ordered (the pipeline + mixer did the mixing,
|
|
14934
|
+
* with `+INF` required cards floated to the front), so the primary path is a
|
|
14935
|
+
* deterministic front-to-back draw — no second new-vs-review mixer. The only
|
|
14936
|
+
* remaining decisions are (a) when the session ends and (b) when to interleave
|
|
14937
|
+
* a remediation card from failedQ. See decision doc §2/§3/§7.
|
|
14819
14938
|
*/
|
|
14820
14939
|
_selectNextItemToHydrate() {
|
|
14821
|
-
|
|
14822
|
-
let newBound = 0.1;
|
|
14823
|
-
let reviewBound = 0.75;
|
|
14824
|
-
if (this.reviewQ.length === 0 && this.failedQ.length === 0 && this.newQ.length === 0) {
|
|
14940
|
+
if (this.supplyQ.length === 0 && this.failedQ.length === 0) {
|
|
14825
14941
|
return null;
|
|
14826
14942
|
}
|
|
14827
14943
|
if (this._secondsRemaining < 2 && this.failedQ.length === 0 && this._minCardsGuarantee <= 0) {
|
|
14828
14944
|
return null;
|
|
14829
14945
|
}
|
|
14830
14946
|
if (this._secondsRemaining <= 0 && this._minCardsGuarantee <= 0) {
|
|
14831
|
-
|
|
14832
|
-
return this.failedQ.peek(0);
|
|
14833
|
-
} else {
|
|
14834
|
-
return null;
|
|
14835
|
-
}
|
|
14836
|
-
}
|
|
14837
|
-
if (this.newQ.dequeueCount < this.sources.length && this.newQ.length) {
|
|
14838
|
-
return this.newQ.peek(0);
|
|
14947
|
+
return this.failedQ.length > 0 ? this.failedQ.peek(0) : null;
|
|
14839
14948
|
}
|
|
14840
|
-
const
|
|
14841
|
-
|
|
14842
|
-
|
|
14843
|
-
if (availableTime > 20) {
|
|
14844
|
-
newBound = 0.5;
|
|
14845
|
-
reviewBound = 0.9;
|
|
14846
|
-
} else if (this._secondsRemaining - cleanupTime > 20) {
|
|
14847
|
-
newBound = 0.05;
|
|
14848
|
-
reviewBound = 0.9;
|
|
14849
|
-
} else {
|
|
14850
|
-
newBound = 0.01;
|
|
14851
|
-
reviewBound = 0.1;
|
|
14949
|
+
const supplyTop = this.supplyQ.length > 0 ? this.supplyQ.peek(0) : null;
|
|
14950
|
+
if (this._minCardsGuarantee > 0 && supplyTop) {
|
|
14951
|
+
return supplyTop;
|
|
14852
14952
|
}
|
|
14853
|
-
if (this.failedQ.length
|
|
14854
|
-
|
|
14953
|
+
if (this.failedQ.length > 0 && this._shouldInterleaveFailed(supplyTop !== null)) {
|
|
14954
|
+
return this.failedQ.peek(0);
|
|
14855
14955
|
}
|
|
14856
|
-
if (
|
|
14857
|
-
|
|
14956
|
+
if (supplyTop) {
|
|
14957
|
+
return supplyTop;
|
|
14858
14958
|
}
|
|
14859
|
-
if (
|
|
14860
|
-
return this.newQ.peek(0);
|
|
14861
|
-
} else if (choice < reviewBound && this.reviewQ.length) {
|
|
14862
|
-
return this.reviewQ.peek(0);
|
|
14863
|
-
} else if (this.failedQ.length) {
|
|
14959
|
+
if (this.failedQ.length > 0) {
|
|
14864
14960
|
return this.failedQ.peek(0);
|
|
14865
|
-
} else {
|
|
14866
|
-
this.log(`No more cards available for the session!`);
|
|
14867
|
-
return null;
|
|
14868
14961
|
}
|
|
14962
|
+
this.log(`No more cards available for the session!`);
|
|
14963
|
+
return null;
|
|
14964
|
+
}
|
|
14965
|
+
/** Supply draws between forced failed-queue interleaves (light steady cadence). */
|
|
14966
|
+
static FAILED_INTERLEAVE_EVERY = 4;
|
|
14967
|
+
/**
|
|
14968
|
+
* Slack (seconds) below which the endgame failed-pressure kicks in: when the
|
|
14969
|
+
* time left after clearing remediation drops under this, bias hard to failed
|
|
14970
|
+
* so the session doesn't end with un-cleared remediation. Mirrors the old
|
|
14971
|
+
* `availableTime > 20` ladder thresholds.
|
|
14972
|
+
*/
|
|
14973
|
+
static FAILED_ENDGAME_SLACK_SECONDS = 20;
|
|
14974
|
+
/**
|
|
14975
|
+
* Whether to interleave a failed (remediation) card now instead of drawing
|
|
14976
|
+
* the supply head. Replaces the old `newBound`/`reviewBound` probability
|
|
14977
|
+
* ladder's failed path (decision doc §7).
|
|
14978
|
+
*
|
|
14979
|
+
* @param supplyAvailable - whether supplyQ has a card to draw instead.
|
|
14980
|
+
*/
|
|
14981
|
+
_shouldInterleaveFailed(supplyAvailable) {
|
|
14982
|
+
if (this.failedQ.length === 0) return false;
|
|
14983
|
+
if (!supplyAvailable) return true;
|
|
14984
|
+
const availableTime = this._secondsRemaining - this.estimateCleanupTime();
|
|
14985
|
+
if (availableTime <= _SessionController.FAILED_ENDGAME_SLACK_SECONDS) return true;
|
|
14986
|
+
return this._supplyDrawsSinceFailed >= _SessionController.FAILED_INTERLEAVE_EVERY;
|
|
14869
14987
|
}
|
|
14870
14988
|
async nextCard(action = "dismiss-success") {
|
|
14871
14989
|
this.dismissCurrentCard(action);
|
|
@@ -14873,22 +14991,21 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14873
14991
|
this._minCardsGuarantee--;
|
|
14874
14992
|
this.log(`[CardGuarantee] ${this._minCardsGuarantee} guaranteed cards remaining`);
|
|
14875
14993
|
}
|
|
14876
|
-
if (this._replanPromise && this.
|
|
14994
|
+
if (this._replanPromise && this.supplyQ.length === 0 && this.failedQ.length === 0) {
|
|
14877
14995
|
this.log("nextCard: queues empty, awaiting in-flight replan before drawing");
|
|
14878
14996
|
await this._replanPromise;
|
|
14879
14997
|
}
|
|
14880
|
-
if (this.
|
|
14998
|
+
if (this.supplyQ.length <= _SessionController.DEPLETION_PREFETCH_THRESHOLD && this._secondsRemaining > 0 && !this._replanPromise) {
|
|
14881
14999
|
this._suppressQualityReplan = false;
|
|
14882
|
-
const otherContent = this.reviewQ.length + this.failedQ.length;
|
|
14883
15000
|
this.log(
|
|
14884
|
-
`[AutoReplan:depletion]
|
|
15001
|
+
`[AutoReplan:depletion] supplyQ has ${this.supplyQ.length} card(s) (${this.failedQ.length} failed pending) with ${this._secondsRemaining}s remaining. Triggering background replan.`
|
|
14885
15002
|
);
|
|
14886
15003
|
void this.requestReplan({ label: "auto:depletion", mode: "merge" });
|
|
14887
15004
|
}
|
|
14888
15005
|
const REPLAN_BUFFER = 3;
|
|
14889
|
-
if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.
|
|
15006
|
+
if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.supplyQ.length > 0 && !this._replanPromise) {
|
|
14890
15007
|
this.log(
|
|
14891
|
-
`[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (
|
|
15008
|
+
`[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (supplyQ: ${this.supplyQ.length}). Triggering background replan.`
|
|
14892
15009
|
);
|
|
14893
15010
|
void this.requestReplan({ label: "auto:quality" });
|
|
14894
15011
|
}
|
|
@@ -14900,12 +15017,12 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14900
15017
|
const WEDGE_MAX_EMPTY_STREAK = 3;
|
|
14901
15018
|
const WEDGE_BACKOFF_MS = 250;
|
|
14902
15019
|
let wedgeEmptyStreak = 0;
|
|
14903
|
-
while (this._secondsRemaining > 0 && this.
|
|
15020
|
+
while (this._secondsRemaining > 0 && this.supplyQ.length === 0 && this.failedQ.length === 0) {
|
|
14904
15021
|
this.log(
|
|
14905
15022
|
`[WedgeBreaker] All queues empty with ${this._secondsRemaining}s remaining. Running pipeline (attempt ${wedgeEmptyStreak + 1}/${WEDGE_MAX_EMPTY_STREAK}).`
|
|
14906
15023
|
);
|
|
14907
15024
|
await this._replanUncoalesced({ label: "wedge-breaker" });
|
|
14908
|
-
if (this.
|
|
15025
|
+
if (this.supplyQ.length === 0 && this.failedQ.length === 0) {
|
|
14909
15026
|
wedgeEmptyStreak++;
|
|
14910
15027
|
if (wedgeEmptyStreak >= WEDGE_MAX_EMPTY_STREAK) {
|
|
14911
15028
|
this.log(
|
|
@@ -14935,15 +15052,16 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14935
15052
|
await this.hydrationService.ensureHydratedCards();
|
|
14936
15053
|
this._currentCard = card;
|
|
14937
15054
|
const origin = nextItem.status === "review" || nextItem.status === "failed-review" ? "review" : nextItem.status === "new" || nextItem.status === "failed-new" ? "new" : "failed";
|
|
14938
|
-
const queueSource = nextItem.status.startsWith("failed") ? "failedQ" :
|
|
15055
|
+
const queueSource = nextItem.status.startsWith("failed") ? "failedQ" : "supplyQ";
|
|
14939
15056
|
recordCardPresentation(
|
|
14940
15057
|
nextItem.cardID,
|
|
14941
15058
|
nextItem.courseID,
|
|
14942
15059
|
this.courseNameCache.get(nextItem.courseID),
|
|
14943
15060
|
origin,
|
|
14944
|
-
queueSource
|
|
15061
|
+
queueSource,
|
|
15062
|
+
nextItem.score
|
|
14945
15063
|
);
|
|
14946
|
-
snapshotQueues(this.
|
|
15064
|
+
snapshotQueues(this.supplyQ.length, this.failedQ.length);
|
|
14947
15065
|
return card;
|
|
14948
15066
|
}
|
|
14949
15067
|
this.log(`Skipping card ${nextItem.cardID}: hydration failed, trying next`);
|
|
@@ -15013,6 +15131,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
15013
15131
|
};
|
|
15014
15132
|
}
|
|
15015
15133
|
this.failedQ.add(failedItem, failedItem.cardID);
|
|
15134
|
+
this._supplyDrawsSinceFailed = 0;
|
|
15016
15135
|
} else if (action === "dismiss-error") {
|
|
15017
15136
|
this.hydrationService.removeCard(this._currentCard.item.cardID);
|
|
15018
15137
|
} else if (action === "dismiss-failed") {
|
|
@@ -15026,15 +15145,15 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
15026
15145
|
removeItemFromQueue(item) {
|
|
15027
15146
|
this._clearDurableRequirement(item.cardID);
|
|
15028
15147
|
this._servedCardIds.add(item.cardID);
|
|
15029
|
-
if (this.
|
|
15030
|
-
this.
|
|
15031
|
-
|
|
15032
|
-
|
|
15148
|
+
if (this.failedQ.peek(0)?.cardID === item.cardID) {
|
|
15149
|
+
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
15150
|
+
this._supplyDrawsSinceFailed = 0;
|
|
15151
|
+
} else if (this.supplyQ.peek(0)?.cardID === item.cardID) {
|
|
15152
|
+
this.supplyQ.dequeue((queueItem) => queueItem.cardID);
|
|
15153
|
+
this._supplyDrawsSinceFailed++;
|
|
15033
15154
|
if (this._wellIndicatedRemaining > 0) {
|
|
15034
15155
|
this._wellIndicatedRemaining--;
|
|
15035
15156
|
}
|
|
15036
|
-
} else if (this.failedQ.peek(0)?.cardID === item.cardID) {
|
|
15037
|
-
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
15038
15157
|
}
|
|
15039
15158
|
}
|
|
15040
15159
|
/**
|
|
@@ -15134,6 +15253,7 @@ init_factory();
|
|
|
15134
15253
|
areQuestionRecords,
|
|
15135
15254
|
buildStrategyStateId,
|
|
15136
15255
|
captureMixerRun,
|
|
15256
|
+
clearSrsBacklogDebug,
|
|
15137
15257
|
computeDeviation,
|
|
15138
15258
|
computeEffectiveWeight,
|
|
15139
15259
|
computeOutcomeSignal,
|
|
@@ -15154,6 +15274,7 @@ init_factory();
|
|
|
15154
15274
|
getRegisteredNavigator,
|
|
15155
15275
|
getRegisteredNavigatorNames,
|
|
15156
15276
|
getRegisteredNavigatorRole,
|
|
15277
|
+
getSrsBacklogDebug,
|
|
15157
15278
|
getStudySource,
|
|
15158
15279
|
hasRegisteredNavigator,
|
|
15159
15280
|
importParsedCards,
|