@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.mjs
CHANGED
|
@@ -1770,6 +1770,30 @@ Example:
|
|
|
1770
1770
|
}
|
|
1771
1771
|
});
|
|
1772
1772
|
|
|
1773
|
+
// src/core/navigators/SrsDebugger.ts
|
|
1774
|
+
var SrsDebugger_exports = {};
|
|
1775
|
+
__export(SrsDebugger_exports, {
|
|
1776
|
+
captureSrsBacklog: () => captureSrsBacklog,
|
|
1777
|
+
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
1778
|
+
getSrsBacklogDebug: () => getSrsBacklogDebug
|
|
1779
|
+
});
|
|
1780
|
+
function captureSrsBacklog(snapshot) {
|
|
1781
|
+
snapshots.set(snapshot.courseId, snapshot);
|
|
1782
|
+
}
|
|
1783
|
+
function getSrsBacklogDebug() {
|
|
1784
|
+
return [...snapshots.values()].sort((a, b) => b.timestamp - a.timestamp);
|
|
1785
|
+
}
|
|
1786
|
+
function clearSrsBacklogDebug() {
|
|
1787
|
+
snapshots.clear();
|
|
1788
|
+
}
|
|
1789
|
+
var snapshots;
|
|
1790
|
+
var init_SrsDebugger = __esm({
|
|
1791
|
+
"src/core/navigators/SrsDebugger.ts"() {
|
|
1792
|
+
"use strict";
|
|
1793
|
+
snapshots = /* @__PURE__ */ new Map();
|
|
1794
|
+
}
|
|
1795
|
+
});
|
|
1796
|
+
|
|
1773
1797
|
// src/core/navigators/generators/CompositeGenerator.ts
|
|
1774
1798
|
var CompositeGenerator_exports = {};
|
|
1775
1799
|
__export(CompositeGenerator_exports, {
|
|
@@ -2137,7 +2161,7 @@ function shuffleInPlace(arr) {
|
|
|
2137
2161
|
function pickTopByScore(cards, limit) {
|
|
2138
2162
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
2139
2163
|
}
|
|
2140
|
-
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;
|
|
2164
|
+
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;
|
|
2141
2165
|
var init_prescribed = __esm({
|
|
2142
2166
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
2143
2167
|
"use strict";
|
|
@@ -2154,6 +2178,9 @@ var init_prescribed = __esm({
|
|
|
2154
2178
|
BASE_SUPPORT_SCORE = 0.8;
|
|
2155
2179
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
2156
2180
|
BASE_PRACTICE_SCORE = 1;
|
|
2181
|
+
PRACTICE_BASE_MULT = 2;
|
|
2182
|
+
MAX_PRACTICE_MULTIPLIER = 4;
|
|
2183
|
+
PRACTICE_STALENESS_BUMP_PER_DAY = 0.5;
|
|
2157
2184
|
MAX_TARGET_MULTIPLIER = 8;
|
|
2158
2185
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
2159
2186
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -2213,6 +2240,8 @@ var init_prescribed = __esm({
|
|
|
2213
2240
|
const emitted = [];
|
|
2214
2241
|
const emittedIds = /* @__PURE__ */ new Set();
|
|
2215
2242
|
const groupRuntimes = [];
|
|
2243
|
+
const priorPracticeDebt = progress.practiceDebt ?? {};
|
|
2244
|
+
const nextPracticeDebt = {};
|
|
2216
2245
|
for (const group of this.config.groups) {
|
|
2217
2246
|
const runtime = this.buildGroupRuntimeState({
|
|
2218
2247
|
group,
|
|
@@ -2270,10 +2299,13 @@ var init_prescribed = __esm({
|
|
|
2270
2299
|
userTagElo,
|
|
2271
2300
|
userGlobalElo,
|
|
2272
2301
|
activeIds,
|
|
2273
|
-
seenIds
|
|
2302
|
+
seenIds,
|
|
2303
|
+
priorPracticeDebt,
|
|
2304
|
+
nextPracticeDebt
|
|
2274
2305
|
});
|
|
2275
2306
|
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
2276
2307
|
}
|
|
2308
|
+
nextState.practiceDebt = nextPracticeDebt;
|
|
2277
2309
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2278
2310
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
2279
2311
|
boostTags: hintSummary.boostTags,
|
|
@@ -2607,9 +2639,16 @@ var init_prescribed = __esm({
|
|
|
2607
2639
|
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2608
2640
|
* into the candidate pool. It exists because global-ELO retrieval
|
|
2609
2641
|
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2610
|
-
* freshly-introduced skill — putting them in the pool here
|
|
2611
|
-
*
|
|
2612
|
-
*
|
|
2642
|
+
* freshly-introduced skill — putting them in the pool here guarantees presence.
|
|
2643
|
+
*
|
|
2644
|
+
* Emphasis is a **practice-debt pressure** (parallel to SRS backlog pressure):
|
|
2645
|
+
* cards score `base × multiplier`, where the multiplier starts at
|
|
2646
|
+
* PRACTICE_BASE_MULT (so a few reps land promptly post-intro, competing with
|
|
2647
|
+
* pressured reviews) and escalates by how long the debt has stayed open
|
|
2648
|
+
* (per-tag, time-based via `priorPracticeDebt`/`nextPracticeDebt`), clamped at
|
|
2649
|
+
* MAX_PRACTICE_MULTIPLIER. The debt is durable and self-discharges the instant
|
|
2650
|
+
* the skill reaches `practiceMinCount` — so this no longer relies on the
|
|
2651
|
+
* session-scoped intro boost to actually surface.
|
|
2613
2652
|
*
|
|
2614
2653
|
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2615
2654
|
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
@@ -2624,7 +2663,9 @@ var init_prescribed = __esm({
|
|
|
2624
2663
|
userTagElo,
|
|
2625
2664
|
userGlobalElo,
|
|
2626
2665
|
activeIds,
|
|
2627
|
-
seenIds
|
|
2666
|
+
seenIds,
|
|
2667
|
+
priorPracticeDebt,
|
|
2668
|
+
nextPracticeDebt
|
|
2628
2669
|
} = args;
|
|
2629
2670
|
const patterns = group.practiceTagPatterns ?? [];
|
|
2630
2671
|
if (patterns.length === 0) return [];
|
|
@@ -2634,6 +2675,20 @@ var init_prescribed = __esm({
|
|
|
2634
2675
|
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2635
2676
|
);
|
|
2636
2677
|
if (practiceTags.length === 0) return [];
|
|
2678
|
+
const now = Date.now();
|
|
2679
|
+
const DAY_MS = 24 * 60 * 60 * 1e3;
|
|
2680
|
+
const tagMultiplier = /* @__PURE__ */ new Map();
|
|
2681
|
+
for (const tag of practiceTags) {
|
|
2682
|
+
const firstOwedAt = priorPracticeDebt[tag] ?? isoNow();
|
|
2683
|
+
nextPracticeDebt[tag] = firstOwedAt;
|
|
2684
|
+
const staleDays = Math.max(0, (now - new Date(firstOwedAt).getTime()) / DAY_MS);
|
|
2685
|
+
const mult = clamp(
|
|
2686
|
+
PRACTICE_BASE_MULT + staleDays * PRACTICE_STALENESS_BUMP_PER_DAY,
|
|
2687
|
+
PRACTICE_BASE_MULT,
|
|
2688
|
+
MAX_PRACTICE_MULTIPLIER
|
|
2689
|
+
);
|
|
2690
|
+
tagMultiplier.set(tag, mult);
|
|
2691
|
+
}
|
|
2637
2692
|
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2638
2693
|
supportTags: practiceTags,
|
|
2639
2694
|
cardsByTag,
|
|
@@ -2649,18 +2704,25 @@ var init_prescribed = __esm({
|
|
|
2649
2704
|
const cards = [];
|
|
2650
2705
|
for (const cardId of practiceCardIds) {
|
|
2651
2706
|
emittedIds.add(cardId);
|
|
2707
|
+
let mult = PRACTICE_BASE_MULT;
|
|
2708
|
+
for (const tag of practiceTags) {
|
|
2709
|
+
if (cardsByTag.get(tag)?.includes(cardId) ?? false) {
|
|
2710
|
+
mult = Math.max(mult, tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT);
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
const score = BASE_PRACTICE_SCORE * mult;
|
|
2652
2714
|
cards.push({
|
|
2653
2715
|
cardId,
|
|
2654
2716
|
courseId,
|
|
2655
|
-
score
|
|
2717
|
+
score,
|
|
2656
2718
|
provenance: [
|
|
2657
2719
|
{
|
|
2658
2720
|
strategy: "prescribed",
|
|
2659
2721
|
strategyName: this.strategyName || this.name,
|
|
2660
2722
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2661
2723
|
action: "generated",
|
|
2662
|
-
score
|
|
2663
|
-
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2724
|
+
score,
|
|
2725
|
+
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}`
|
|
2664
2726
|
}
|
|
2665
2727
|
]
|
|
2666
2728
|
});
|
|
@@ -2834,14 +2896,15 @@ __export(srs_exports, {
|
|
|
2834
2896
|
default: () => SRSNavigator
|
|
2835
2897
|
});
|
|
2836
2898
|
import moment3 from "moment";
|
|
2837
|
-
var DEFAULT_HEALTHY_BACKLOG,
|
|
2899
|
+
var DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_MULTIPLIER, SRSNavigator;
|
|
2838
2900
|
var init_srs = __esm({
|
|
2839
2901
|
"src/core/navigators/generators/srs.ts"() {
|
|
2840
2902
|
"use strict";
|
|
2841
2903
|
init_navigators();
|
|
2904
|
+
init_SrsDebugger();
|
|
2842
2905
|
init_logger();
|
|
2843
2906
|
DEFAULT_HEALTHY_BACKLOG = 20;
|
|
2844
|
-
|
|
2907
|
+
MAX_BACKLOG_MULTIPLIER = 2;
|
|
2845
2908
|
SRSNavigator = class extends ContentNavigator {
|
|
2846
2909
|
/** Human-readable name for CardGenerator interface */
|
|
2847
2910
|
name;
|
|
@@ -2908,9 +2971,18 @@ var init_srs = __esm({
|
|
|
2908
2971
|
}
|
|
2909
2972
|
}
|
|
2910
2973
|
}
|
|
2911
|
-
const
|
|
2974
|
+
const backlogMultiplier = this.computeBacklogMultiplier(dueReviews.length);
|
|
2975
|
+
const notDue = reviews.filter((r) => !now.isAfter(moment3.utc(r.reviewTime)));
|
|
2976
|
+
let nextDueIn = null;
|
|
2977
|
+
if (notDue.length > 0) {
|
|
2978
|
+
const next = notDue.reduce(
|
|
2979
|
+
(a, b) => moment3.utc(a.reviewTime).isBefore(moment3.utc(b.reviewTime)) ? a : b
|
|
2980
|
+
);
|
|
2981
|
+
const until = moment3.duration(moment3.utc(next.reviewTime).diff(now));
|
|
2982
|
+
nextDueIn = until.asHours() < 1 ? `${Math.round(until.asMinutes())}m` : until.asHours() < 24 ? `${Math.round(until.asHours())}h` : `${Math.round(until.asDays())}d`;
|
|
2983
|
+
}
|
|
2912
2984
|
if (dueReviews.length > 0) {
|
|
2913
|
-
const pressureNote =
|
|
2985
|
+
const pressureNote = backlogMultiplier > 1 ? ` [backlog pressure: \xD7${backlogMultiplier.toFixed(2)}]` : ` [healthy backlog]`;
|
|
2914
2986
|
logger.info(
|
|
2915
2987
|
`[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
|
|
2916
2988
|
);
|
|
@@ -2929,7 +3001,7 @@ var init_srs = __esm({
|
|
|
2929
3001
|
logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
|
|
2930
3002
|
}
|
|
2931
3003
|
const scored = dueReviews.map((review) => {
|
|
2932
|
-
const { score, reason } = this.computeUrgencyScore(review, now,
|
|
3004
|
+
const { score, reason } = this.computeUrgencyScore(review, now, backlogMultiplier);
|
|
2933
3005
|
return {
|
|
2934
3006
|
cardId: review.cardId,
|
|
2935
3007
|
courseId: review.courseId,
|
|
@@ -2947,30 +3019,42 @@ var init_srs = __esm({
|
|
|
2947
3019
|
]
|
|
2948
3020
|
};
|
|
2949
3021
|
});
|
|
2950
|
-
|
|
3022
|
+
const sorted = scored.sort((a, b) => b.score - a.score);
|
|
3023
|
+
captureSrsBacklog({
|
|
3024
|
+
courseId,
|
|
3025
|
+
scheduledTotal: reviews.length,
|
|
3026
|
+
dueNow: dueReviews.length,
|
|
3027
|
+
healthyBacklog: this.healthyBacklog,
|
|
3028
|
+
backlogMultiplier,
|
|
3029
|
+
maxBacklogMultiplier: MAX_BACKLOG_MULTIPLIER,
|
|
3030
|
+
topReviewScore: sorted.length > 0 ? sorted[0].score : null,
|
|
3031
|
+
nextDueIn,
|
|
3032
|
+
timestamp: Date.now()
|
|
3033
|
+
});
|
|
3034
|
+
return { cards: sorted.slice(0, limit) };
|
|
2951
3035
|
}
|
|
2952
3036
|
/**
|
|
2953
|
-
* Compute backlog pressure based on number of due reviews.
|
|
3037
|
+
* Compute the multiplicative backlog pressure based on number of due reviews.
|
|
2954
3038
|
*
|
|
2955
|
-
*
|
|
2956
|
-
* and
|
|
3039
|
+
* ×1.0 at or below the healthy threshold (no boost), increasing linearly above
|
|
3040
|
+
* it and maxing out at MAX_BACKLOG_MULTIPLIER at 3× the healthy backlog.
|
|
2957
3041
|
*
|
|
2958
|
-
* Examples (with default healthyBacklog=20):
|
|
2959
|
-
* - 10 due reviews →
|
|
2960
|
-
* - 20 due reviews →
|
|
2961
|
-
* - 40 due reviews →
|
|
2962
|
-
* - 60 due reviews →
|
|
3042
|
+
* Examples (with default healthyBacklog=20, MAX_BACKLOG_MULTIPLIER=2.0):
|
|
3043
|
+
* - 10 due reviews → ×1.00 (healthy)
|
|
3044
|
+
* - 20 due reviews → ×1.00 (at threshold)
|
|
3045
|
+
* - 40 due reviews → ×1.50 (2x threshold)
|
|
3046
|
+
* - 60 due reviews → ×2.00 (3x threshold, maxed)
|
|
2963
3047
|
*
|
|
2964
3048
|
* @param dueCount - Number of reviews currently due
|
|
2965
|
-
* @returns
|
|
3049
|
+
* @returns Multiplier applied to review urgency (1.0 to MAX_BACKLOG_MULTIPLIER)
|
|
2966
3050
|
*/
|
|
2967
|
-
|
|
3051
|
+
computeBacklogMultiplier(dueCount) {
|
|
2968
3052
|
if (dueCount <= this.healthyBacklog) {
|
|
2969
|
-
return
|
|
3053
|
+
return 1;
|
|
2970
3054
|
}
|
|
2971
3055
|
const excess = dueCount - this.healthyBacklog;
|
|
2972
|
-
const
|
|
2973
|
-
return Math.min(
|
|
3056
|
+
const multiplier = 1 + excess / this.healthyBacklog * ((MAX_BACKLOG_MULTIPLIER - 1) / 2);
|
|
3057
|
+
return Math.min(MAX_BACKLOG_MULTIPLIER, multiplier);
|
|
2974
3058
|
}
|
|
2975
3059
|
/**
|
|
2976
3060
|
* Compute urgency score for a review card.
|
|
@@ -2985,19 +3069,20 @@ var init_srs = __esm({
|
|
|
2985
3069
|
* - 30 days (720h) → ~0.56
|
|
2986
3070
|
* - 180 days → ~0.30
|
|
2987
3071
|
*
|
|
2988
|
-
* 3. Backlog pressure = global
|
|
2989
|
-
*
|
|
2990
|
-
* - At 2x healthy: +0.25
|
|
2991
|
-
* - At 3x+ healthy: +0.50 (max)
|
|
3072
|
+
* 3. Backlog pressure = global *multiplier* when review backlog exceeds the
|
|
3073
|
+
* healthy threshold (×1.0 healthy → up to MAX_BACKLOG_MULTIPLIER at 3×).
|
|
2992
3074
|
*
|
|
2993
|
-
* Combined: base 0.5 +
|
|
2994
|
-
*
|
|
3075
|
+
* Combined: (base 0.5 + urgency factors * 0.45) × backlog multiplier.
|
|
3076
|
+
* Per-card range before pressure: ~0.57–0.95. NOT clamped to 1.0 — under a
|
|
3077
|
+
* heavy backlog reviews scale onto the open scale to compete with (and exceed)
|
|
3078
|
+
* new cards; what keeps them from running away is the bounded multiplier, not
|
|
3079
|
+
* a hard ceiling.
|
|
2995
3080
|
*
|
|
2996
3081
|
* @param review - The scheduled card to score
|
|
2997
3082
|
* @param now - Current time
|
|
2998
|
-
* @param
|
|
3083
|
+
* @param backlogMultiplier - Pre-computed backlog multiplier (1.0 to MAX_BACKLOG_MULTIPLIER)
|
|
2999
3084
|
*/
|
|
3000
|
-
computeUrgencyScore(review, now,
|
|
3085
|
+
computeUrgencyScore(review, now, backlogMultiplier) {
|
|
3001
3086
|
const scheduledAt = moment3.utc(review.scheduledAt);
|
|
3002
3087
|
const due = moment3.utc(review.reviewTime);
|
|
3003
3088
|
const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
|
|
@@ -3007,15 +3092,15 @@ var init_srs = __esm({
|
|
|
3007
3092
|
const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
|
|
3008
3093
|
const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
|
|
3009
3094
|
const baseScore = 0.5 + urgency * 0.45;
|
|
3010
|
-
const score =
|
|
3095
|
+
const score = baseScore * backlogMultiplier;
|
|
3011
3096
|
const reasonParts = [
|
|
3012
3097
|
`${Math.round(hoursOverdue)}h overdue`,
|
|
3013
3098
|
`interval: ${Math.round(intervalHours)}h`,
|
|
3014
3099
|
`relative: ${relativeOverdue.toFixed(2)}`,
|
|
3015
3100
|
`recency: ${recencyFactor.toFixed(2)}`
|
|
3016
3101
|
];
|
|
3017
|
-
if (
|
|
3018
|
-
reasonParts.push(`backlog:
|
|
3102
|
+
if (backlogMultiplier > 1) {
|
|
3103
|
+
reasonParts.push(`backlog: \xD7${backlogMultiplier.toFixed(2)}`);
|
|
3019
3104
|
}
|
|
3020
3105
|
reasonParts.push("review");
|
|
3021
3106
|
const reason = reasonParts.join(", ");
|
|
@@ -5313,6 +5398,7 @@ var init_3 = __esm({
|
|
|
5313
5398
|
"./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
|
|
5314
5399
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
5315
5400
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
5401
|
+
"./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
|
|
5316
5402
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5317
5403
|
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
5318
5404
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
@@ -5345,12 +5431,14 @@ __export(navigators_exports, {
|
|
|
5345
5431
|
NavigatorRole: () => NavigatorRole,
|
|
5346
5432
|
NavigatorRoles: () => NavigatorRoles,
|
|
5347
5433
|
Navigators: () => Navigators,
|
|
5434
|
+
clearSrsBacklogDebug: () => clearSrsBacklogDebug,
|
|
5348
5435
|
diversityRerank: () => diversityRerank,
|
|
5349
5436
|
getActivePipeline: () => getActivePipeline,
|
|
5350
5437
|
getCardOrigin: () => getCardOrigin,
|
|
5351
5438
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
5352
5439
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
5353
5440
|
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
5441
|
+
getSrsBacklogDebug: () => getSrsBacklogDebug,
|
|
5354
5442
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
5355
5443
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
5356
5444
|
isFilter: () => isFilter,
|
|
@@ -5432,6 +5520,7 @@ var init_navigators = __esm({
|
|
|
5432
5520
|
"use strict";
|
|
5433
5521
|
init_diversityRerank();
|
|
5434
5522
|
init_PipelineDebugger();
|
|
5523
|
+
init_SrsDebugger();
|
|
5435
5524
|
init_logger();
|
|
5436
5525
|
init_();
|
|
5437
5526
|
init_2();
|
|
@@ -6039,9 +6128,10 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
6039
6128
|
return tagsByCard;
|
|
6040
6129
|
}
|
|
6041
6130
|
async getAllCardIds() {
|
|
6131
|
+
const prefix = `${DocTypePrefixes["CARD" /* CARD */]}-`;
|
|
6042
6132
|
const result = await this.db.allDocs({
|
|
6043
|
-
startkey:
|
|
6044
|
-
endkey:
|
|
6133
|
+
startkey: prefix,
|
|
6134
|
+
endkey: `${prefix}\uFFF0`,
|
|
6045
6135
|
include_docs: false
|
|
6046
6136
|
});
|
|
6047
6137
|
return result.rows.map((row) => row.id);
|
|
@@ -13338,6 +13428,7 @@ mountMixerDebugger();
|
|
|
13338
13428
|
// src/study/SessionDebugger.ts
|
|
13339
13429
|
init_logger();
|
|
13340
13430
|
init_PipelineDebugger();
|
|
13431
|
+
init_SrsDebugger();
|
|
13341
13432
|
|
|
13342
13433
|
// src/study/SessionOverlay.ts
|
|
13343
13434
|
init_logger();
|
|
@@ -13359,8 +13450,7 @@ var lastSnapshot = null;
|
|
|
13359
13450
|
var copyFlashUntil = 0;
|
|
13360
13451
|
var minified = false;
|
|
13361
13452
|
var expanded = {
|
|
13362
|
-
|
|
13363
|
-
newQ: false,
|
|
13453
|
+
supplyQ: false,
|
|
13364
13454
|
failedQ: false,
|
|
13365
13455
|
drawn: false
|
|
13366
13456
|
};
|
|
@@ -13429,7 +13519,7 @@ function render() {
|
|
|
13429
13519
|
attachHandlers();
|
|
13430
13520
|
return;
|
|
13431
13521
|
}
|
|
13432
|
-
overlayEl.innerHTML = headerHtml() + replanHtml(s) + metaHtml(s) + hintsHtml(s.sessionHints) +
|
|
13522
|
+
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);
|
|
13433
13523
|
attachHandlers();
|
|
13434
13524
|
}
|
|
13435
13525
|
function attachHandlers() {
|
|
@@ -13521,6 +13611,29 @@ function hintsHtml(h) {
|
|
|
13521
13611
|
const body = parts.length ? parts.map((p) => `<div style="margin-left:6px">${p}</div>`).join("") : `<div style="margin-left:6px;opacity:.6">none</div>`;
|
|
13522
13612
|
return `<div style="margin-bottom:6px"><div style="color:#86efac">sessionHints</div>${body}</div>`;
|
|
13523
13613
|
}
|
|
13614
|
+
function backlogHtml(backlog) {
|
|
13615
|
+
if (!backlog.length) return "";
|
|
13616
|
+
const rows = backlog.map((b) => {
|
|
13617
|
+
const maxed = b.backlogMultiplier >= b.maxBacklogMultiplier - 1e-9;
|
|
13618
|
+
const multColor = b.backlogMultiplier <= 1 ? "#86efac" : maxed ? "#fca5a5" : "#fcd34d";
|
|
13619
|
+
const headroom = maxed ? "maxed \u2014 boosts decide order until they relax" : b.backlogMultiplier > 1 ? "climbing as backlog grows" : "healthy \u2014 no pressure";
|
|
13620
|
+
const top = b.topReviewScore !== null ? b.topReviewScore.toFixed(2) : "\u2014";
|
|
13621
|
+
const next = b.nextDueIn ? ` <span style="opacity:.6">\xB7 next due ${esc(b.nextDueIn)}</span>` : "";
|
|
13622
|
+
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>`;
|
|
13623
|
+
}).join("");
|
|
13624
|
+
return `<div style="margin-bottom:6px"><div style="color:#93c5fd">review backpressure</div>${rows}</div>`;
|
|
13625
|
+
}
|
|
13626
|
+
function fmtScore(score) {
|
|
13627
|
+
if (score === void 0) return "";
|
|
13628
|
+
if (!Number.isFinite(score)) return "REQ";
|
|
13629
|
+
return score.toFixed(2);
|
|
13630
|
+
}
|
|
13631
|
+
function queueItemHtml(item) {
|
|
13632
|
+
const tagColor = item.origin === "review" ? "#93c5fd" : "#fcd34d";
|
|
13633
|
+
const score = fmtScore(item.score);
|
|
13634
|
+
const label = `${item.origin === "review" ? "r" : "n"}${score ? " " + score : ""}`;
|
|
13635
|
+
return `${esc(item.cardID)}<span style="color:${tagColor};opacity:.85"> [${label}]</span>`;
|
|
13636
|
+
}
|
|
13524
13637
|
function queueHtml(key, label, q) {
|
|
13525
13638
|
const collapsible = q.length > INLINE_THRESHOLD;
|
|
13526
13639
|
const isOpen = collapsible && expanded[key];
|
|
@@ -13535,7 +13648,7 @@ function queueHtml(key, label, q) {
|
|
|
13535
13648
|
const shown = isOpen ? q.cards : q.cards.slice(0, INLINE_THRESHOLD);
|
|
13536
13649
|
const hiddenCount = q.length - shown.length;
|
|
13537
13650
|
const listMarginBottom = collapsible ? 2 : 6;
|
|
13538
|
-
let body = `<ol style="margin:2px 0 ${listMarginBottom}px 0;padding-left:20px">` + shown.map((c) => `<li style="white-space:nowrap">${
|
|
13651
|
+
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>`;
|
|
13539
13652
|
if (collapsible) {
|
|
13540
13653
|
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13541
13654
|
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
@@ -13598,13 +13711,29 @@ function snapshotToText(s) {
|
|
|
13598
13711
|
if (h.excludeCards?.length) hintParts.push(` excludeCards: ${h.excludeCards.join(", ")}`);
|
|
13599
13712
|
}
|
|
13600
13713
|
lines.push(hintParts.length ? hintParts.join("\n") : " none");
|
|
13714
|
+
if (s.reviewBacklog.length) {
|
|
13715
|
+
lines.push("");
|
|
13716
|
+
lines.push("review backpressure:");
|
|
13717
|
+
for (const b of s.reviewBacklog) {
|
|
13718
|
+
const maxed = b.backlogMultiplier >= b.maxBacklogMultiplier - 1e-9;
|
|
13719
|
+
const headroom = maxed ? "maxed (boosts decide order)" : b.backlogMultiplier > 1 ? "climbing" : "healthy";
|
|
13720
|
+
const top = b.topReviewScore !== null ? b.topReviewScore.toFixed(2) : "\u2014";
|
|
13721
|
+
const next = b.nextDueIn ? `, next due ${b.nextDueIn}` : "";
|
|
13722
|
+
lines.push(
|
|
13723
|
+
` ${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}`
|
|
13724
|
+
);
|
|
13725
|
+
}
|
|
13726
|
+
}
|
|
13601
13727
|
const queueText = (label, q) => {
|
|
13602
13728
|
lines.push("");
|
|
13603
13729
|
lines.push(`${label}: ${q.length} (drawn ${q.dequeueCount})`);
|
|
13604
|
-
q.cards.forEach((c, i) =>
|
|
13730
|
+
q.cards.forEach((c, i) => {
|
|
13731
|
+
const score = fmtScore(c.score);
|
|
13732
|
+
const tag = `${c.origin === "review" ? "r" : "n"}${score ? " " + score : ""}`;
|
|
13733
|
+
lines.push(` ${i + 1}. ${c.cardID} [${tag}]`);
|
|
13734
|
+
});
|
|
13605
13735
|
};
|
|
13606
|
-
queueText("
|
|
13607
|
-
queueText("newQ", s.newQ);
|
|
13736
|
+
queueText("supplyQ", s.supplyQ);
|
|
13608
13737
|
queueText("failedQ", s.failedQ);
|
|
13609
13738
|
lines.push("");
|
|
13610
13739
|
lines.push(`drawn: ${s.drawnCards.length}`);
|
|
@@ -13629,16 +13758,16 @@ function esc(value) {
|
|
|
13629
13758
|
var activeSession = null;
|
|
13630
13759
|
var sessionHistory = [];
|
|
13631
13760
|
var MAX_HISTORY = 5;
|
|
13632
|
-
function startSessionTracking(
|
|
13761
|
+
function startSessionTracking(supplyQLength, failedQLength) {
|
|
13633
13762
|
clearRunHistory();
|
|
13763
|
+
clearSrsBacklogDebug();
|
|
13634
13764
|
const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
13635
13765
|
activeSession = {
|
|
13636
13766
|
sessionId,
|
|
13637
13767
|
startTime: /* @__PURE__ */ new Date(),
|
|
13638
13768
|
initialQueues: {
|
|
13639
13769
|
timestamp: /* @__PURE__ */ new Date(),
|
|
13640
|
-
|
|
13641
|
-
newQLength,
|
|
13770
|
+
supplyQLength,
|
|
13642
13771
|
failedQLength
|
|
13643
13772
|
},
|
|
13644
13773
|
presentations: [],
|
|
@@ -13662,17 +13791,15 @@ function recordCardPresentation(cardId, courseId, courseName, origin, queueSourc
|
|
|
13662
13791
|
score
|
|
13663
13792
|
});
|
|
13664
13793
|
}
|
|
13665
|
-
function snapshotQueues(
|
|
13794
|
+
function snapshotQueues(supplyQLength, failedQLength, supplyQNext3) {
|
|
13666
13795
|
if (!activeSession) {
|
|
13667
13796
|
return;
|
|
13668
13797
|
}
|
|
13669
13798
|
activeSession.queueSnapshots.push({
|
|
13670
13799
|
timestamp: /* @__PURE__ */ new Date(),
|
|
13671
|
-
|
|
13672
|
-
newQLength,
|
|
13800
|
+
supplyQLength,
|
|
13673
13801
|
failedQLength,
|
|
13674
|
-
|
|
13675
|
-
newQNext3
|
|
13802
|
+
supplyQNext3
|
|
13676
13803
|
});
|
|
13677
13804
|
}
|
|
13678
13805
|
function endSessionTracking() {
|
|
@@ -13694,13 +13821,9 @@ function showCurrentQueue() {
|
|
|
13694
13821
|
}
|
|
13695
13822
|
const latest = activeSession.queueSnapshots[activeSession.queueSnapshots.length - 1] || activeSession.initialQueues;
|
|
13696
13823
|
console.group("\u{1F4CA} Current Queue State");
|
|
13697
|
-
logger.info(`
|
|
13698
|
-
if (latest.
|
|
13699
|
-
logger.info(` Next: ${latest.
|
|
13700
|
-
}
|
|
13701
|
-
logger.info(`New Queue: ${latest.newQLength} cards`);
|
|
13702
|
-
if (latest.newQNext3 && latest.newQNext3.length > 0) {
|
|
13703
|
-
logger.info(` Next: ${latest.newQNext3.join(", ")}`);
|
|
13824
|
+
logger.info(`Supply Queue: ${latest.supplyQLength} cards`);
|
|
13825
|
+
if (latest.supplyQNext3 && latest.supplyQNext3.length > 0) {
|
|
13826
|
+
logger.info(` Next: ${latest.supplyQNext3.join(", ")}`);
|
|
13704
13827
|
}
|
|
13705
13828
|
logger.info(`Failed Queue: ${latest.failedQLength} cards`);
|
|
13706
13829
|
console.groupEnd();
|
|
@@ -13917,15 +14040,6 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13917
14040
|
* Individual replans can override via `ReplanOptions.limit`.
|
|
13918
14041
|
*/
|
|
13919
14042
|
_defaultBatchLimit = 20;
|
|
13920
|
-
/**
|
|
13921
|
-
* Maximum number of reviews enqueued at session start. Reviews live
|
|
13922
|
-
* outside the replan flow — the queue drains via consumption and is
|
|
13923
|
-
* not refilled mid-session. The session timer caps total review
|
|
13924
|
-
* exposure, so overfilling here is intentional. Default is generous
|
|
13925
|
-
* to accommodate Anki-style power users with hundreds of due reviews;
|
|
13926
|
-
* apps targeting nimbler sessions should override via constructor.
|
|
13927
|
-
*/
|
|
13928
|
-
_initialReviewCap = 200;
|
|
13929
14043
|
sources;
|
|
13930
14044
|
// dataLayer and getViewComponent now injected into CardHydrationService
|
|
13931
14045
|
_sessionRecord = [];
|
|
@@ -13934,10 +14048,28 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13934
14048
|
}
|
|
13935
14049
|
// Session card stores
|
|
13936
14050
|
_currentCard = null;
|
|
13937
|
-
|
|
13938
|
-
|
|
14051
|
+
/**
|
|
14052
|
+
* The single supply queue: `new` + `review` items interleaved in pipeline
|
|
14053
|
+
* rank order (the mixer's score-ordered, source-interleaved output, with
|
|
14054
|
+
* `+INF` required cards floated to the front). Drawn front-to-back; reviews
|
|
14055
|
+
* and new compete on one cross-comparable scale rather than being re-mixed
|
|
14056
|
+
* by a probability gate. Replaced/re-ranked wholesale on replan. See
|
|
14057
|
+
* `docs/decision-single-supply-queue.md`.
|
|
14058
|
+
*/
|
|
14059
|
+
supplyQ = new ItemQueue();
|
|
13939
14060
|
failedQ = new ItemQueue();
|
|
13940
14061
|
// END Session card stores
|
|
14062
|
+
/**
|
|
14063
|
+
* Supply draws since the last failed-queue *event* (a failed draw, or a card
|
|
14064
|
+
* entering failedQ on failure). Drives the light steady failed-interleave
|
|
14065
|
+
* (§7): after this many consecutive supply draws, a pending failed card is
|
|
14066
|
+
* drawn so remediation doesn't starve mid-session. Incremented on each supply
|
|
14067
|
+
* draw; reset to 0 both when a failed card is drawn AND when one is added to
|
|
14068
|
+
* failedQ — the latter gives a just-failed card spacing instead of an instant
|
|
14069
|
+
* retry (the counter would otherwise already be ≥ threshold from the preceding
|
|
14070
|
+
* supply run).
|
|
14071
|
+
*/
|
|
14072
|
+
_supplyDrawsSinceFailed = 0;
|
|
13941
14073
|
/**
|
|
13942
14074
|
* Promise tracking a currently in-progress replan, or null if idle.
|
|
13943
14075
|
* Used by nextCard() to await completion before drawing from queues.
|
|
@@ -13951,8 +14083,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13951
14083
|
*/
|
|
13952
14084
|
_activeReplanLabel = null;
|
|
13953
14085
|
/**
|
|
13954
|
-
* Number of well-indicated
|
|
13955
|
-
* degrades to poorly-indicated content. Decremented on each
|
|
14086
|
+
* Number of well-indicated supply cards remaining before the queue
|
|
14087
|
+
* degrades to poorly-indicated content. Decremented on each supplyQ
|
|
13956
14088
|
* draw; when it hits 0, a replan is triggered automatically
|
|
13957
14089
|
* (user state has changed from completing good cards).
|
|
13958
14090
|
*/
|
|
@@ -13961,7 +14093,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13961
14093
|
* When true, suppresses the quality-based auto-replan trigger in
|
|
13962
14094
|
* nextCard(). Set after a burst replan (small limit) to prevent the
|
|
13963
14095
|
* auto-replan from clobbering the burst cards before they're consumed.
|
|
13964
|
-
* Cleared when the depletion-triggered replan fires (
|
|
14096
|
+
* Cleared when the depletion-triggered replan fires (supplyQ exhausted).
|
|
13965
14097
|
*/
|
|
13966
14098
|
_suppressQualityReplan = false;
|
|
13967
14099
|
/**
|
|
@@ -13990,13 +14122,15 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13990
14122
|
* a draw the instant it happens — earlier than `_sessionRecord`, which only
|
|
13991
14123
|
* lands once the card is *responded to*.
|
|
13992
14124
|
*
|
|
13993
|
-
* Used to keep already-served cards out of
|
|
13994
|
-
* card shown once must never re-enter
|
|
13995
|
-
*
|
|
13996
|
-
*
|
|
13997
|
-
*
|
|
13998
|
-
*
|
|
13999
|
-
*
|
|
14125
|
+
* Used to keep already-served cards out of supplyQ on every (re)plan, across
|
|
14126
|
+
* ALL origins: a `new` card shown once must never re-enter, and once replans
|
|
14127
|
+
* re-pull reviews, an answered/in-flight review must not re-enter the supply
|
|
14128
|
+
* before its SRS reschedule clears the due-window (the review-loop guard,
|
|
14129
|
+
* decision doc §4). This is the general guard against re-presentation —
|
|
14130
|
+
* including the case where a replan in flight captured a now-drawn card (e.g.
|
|
14131
|
+
* a +INF require-injected follow-up the depletion prefetch grabbed just before
|
|
14132
|
+
* it was drawn). failedQ is separate and controller-owned, so failed cards
|
|
14133
|
+
* legitimately recur there without being gated here.
|
|
14000
14134
|
*/
|
|
14001
14135
|
_servedCardIds = /* @__PURE__ */ new Set();
|
|
14002
14136
|
/**
|
|
@@ -14021,14 +14155,12 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14021
14155
|
return this._minCardsGuarantee > 0;
|
|
14022
14156
|
}
|
|
14023
14157
|
get report() {
|
|
14024
|
-
const
|
|
14025
|
-
const
|
|
14026
|
-
|
|
14027
|
-
const newCardWord = newCount === 1 ? "new card" : "new cards";
|
|
14028
|
-
return `${reviewCount} ${reviewWord}, ${newCount} ${newCardWord}`;
|
|
14158
|
+
const supplyCount = this.supplyQ.dequeueCount;
|
|
14159
|
+
const supplyWord = supplyCount === 1 ? "card" : "cards";
|
|
14160
|
+
return `${supplyCount} supply ${supplyWord} drawn`;
|
|
14029
14161
|
}
|
|
14030
14162
|
get detailedReport() {
|
|
14031
|
-
return this.
|
|
14163
|
+
return this.supplyQ.toString + "\n" + this.failedQ.toString;
|
|
14032
14164
|
}
|
|
14033
14165
|
// @ts-expect-error NodeJS.Timeout type not available in browser context
|
|
14034
14166
|
_intervalHandle;
|
|
@@ -14039,11 +14171,9 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14039
14171
|
* @param getViewComponent - Function to resolve view components
|
|
14040
14172
|
* @param mixer - Optional source mixer strategy (defaults to QuotaRoundRobinMixer)
|
|
14041
14173
|
* @param options - Optional session-level configuration
|
|
14042
|
-
* @param options.defaultBatchLimit - Default
|
|
14174
|
+
* @param options.defaultBatchLimit - Default supply working-set size (default: 20).
|
|
14043
14175
|
* Smaller values for newer users cause more frequent replans, keeping plans
|
|
14044
14176
|
* aligned with rapidly-changing user state.
|
|
14045
|
-
* @param options.initialReviewCap - Max reviews loaded at session start (default: 200).
|
|
14046
|
-
* Applied only on initial planning; replans do not refill the review queue.
|
|
14047
14177
|
*/
|
|
14048
14178
|
constructor(sources, time, dataLayer, getViewComponent, mixer, options) {
|
|
14049
14179
|
super();
|
|
@@ -14066,17 +14196,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14066
14196
|
if (options?.defaultBatchLimit !== void 0) {
|
|
14067
14197
|
this._defaultBatchLimit = options.defaultBatchLimit;
|
|
14068
14198
|
}
|
|
14069
|
-
if (options?.initialReviewCap !== void 0) {
|
|
14070
|
-
this._initialReviewCap = options.initialReviewCap;
|
|
14071
|
-
}
|
|
14072
14199
|
if (options?.outcomeObservers?.length) {
|
|
14073
14200
|
this._outcomeObservers = [...options.outcomeObservers];
|
|
14074
14201
|
}
|
|
14075
14202
|
this.log(`Session constructed:
|
|
14076
14203
|
startTime: ${this.startTime}
|
|
14077
14204
|
endTime: ${this.endTime}
|
|
14078
|
-
defaultBatchLimit: ${this._defaultBatchLimit}
|
|
14079
|
-
initialReviewCap: ${this._initialReviewCap}`);
|
|
14205
|
+
defaultBatchLimit: ${this._defaultBatchLimit}`);
|
|
14080
14206
|
registerActiveController(this);
|
|
14081
14207
|
}
|
|
14082
14208
|
tick() {
|
|
@@ -14110,15 +14236,6 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14110
14236
|
this.log(`Failed card cleanup estimate: ${Math.round(ret)}`);
|
|
14111
14237
|
return ret;
|
|
14112
14238
|
}
|
|
14113
|
-
/**
|
|
14114
|
-
* Extremely rough, conservative, estimate of amound of time to complete
|
|
14115
|
-
* all scheduled reviews
|
|
14116
|
-
*/
|
|
14117
|
-
estimateReviewTime() {
|
|
14118
|
-
const ret = 5 * this.reviewQ.length;
|
|
14119
|
-
this.log(`Review card time estimate: ${ret}`);
|
|
14120
|
-
return ret;
|
|
14121
|
-
}
|
|
14122
14239
|
async prepareSession() {
|
|
14123
14240
|
if (this.sources.some((s) => typeof s.getWeightedCards !== "function")) {
|
|
14124
14241
|
throw new Error(
|
|
@@ -14133,15 +14250,15 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14133
14250
|
);
|
|
14134
14251
|
}
|
|
14135
14252
|
await this.hydrationService.ensureHydratedCards();
|
|
14136
|
-
startSessionTracking(this.
|
|
14253
|
+
startSessionTracking(this.supplyQ.length, this.failedQ.length);
|
|
14137
14254
|
this._intervalHandle = setInterval(() => {
|
|
14138
14255
|
this.tick();
|
|
14139
14256
|
}, 1e3);
|
|
14140
14257
|
}
|
|
14141
14258
|
/**
|
|
14142
14259
|
* Request a mid-session replan. Re-runs the pipeline with current user state
|
|
14143
|
-
* and atomically replaces the
|
|
14144
|
-
* a session.
|
|
14260
|
+
* and atomically replaces (or merges into) the supplyQ contents. Safe to call
|
|
14261
|
+
* at any time during a session.
|
|
14145
14262
|
*
|
|
14146
14263
|
* Concurrency policy:
|
|
14147
14264
|
* - Two unhinted auto-replans never run in parallel; the second coalesces
|
|
@@ -14155,7 +14272,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14155
14272
|
* results (e.g. surfacing another gpc-intro card right after one
|
|
14156
14273
|
* completed, skipping the prescribed `c-wst-*` follow-up).
|
|
14157
14274
|
*
|
|
14158
|
-
*
|
|
14275
|
+
* Re-pulls and re-ranks the whole supply (including reviews); does NOT affect
|
|
14276
|
+
* failedQ (controller-owned remediation).
|
|
14159
14277
|
*
|
|
14160
14278
|
* If nextCard() is called while a replan is in flight, it will automatically
|
|
14161
14279
|
* await the replan before drawing from queues, ensuring the user always sees
|
|
@@ -14221,7 +14339,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14221
14339
|
* excludeCards happen at *invocation* time, not at *queue* time. For a
|
|
14222
14340
|
* queued replan that means excludes reflect the state after the prior
|
|
14223
14341
|
* replan landed — which is what we want, since the prior replan's
|
|
14224
|
-
*
|
|
14342
|
+
* supplyQ.peek(0) is the imminent draw we need to exclude.
|
|
14225
14343
|
*/
|
|
14226
14344
|
async _runReplan(opts) {
|
|
14227
14345
|
this._activeReplanLabel = opts.label ?? "(auto)";
|
|
@@ -14234,8 +14352,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14234
14352
|
for (const rec of this._sessionRecord) {
|
|
14235
14353
|
excludeSet.add(rec.card.card_id);
|
|
14236
14354
|
}
|
|
14237
|
-
if (this.
|
|
14238
|
-
excludeSet.add(this.
|
|
14355
|
+
if (this.supplyQ.length > 0) {
|
|
14356
|
+
excludeSet.add(this.supplyQ.peek(0).cardID);
|
|
14239
14357
|
}
|
|
14240
14358
|
hints.excludeCards = [...excludeSet];
|
|
14241
14359
|
if (opts.sessionHints !== void 0) {
|
|
@@ -14296,7 +14414,13 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14296
14414
|
const describe = (q) => {
|
|
14297
14415
|
const cards = [];
|
|
14298
14416
|
for (let i = 0; i < q.length; i++) {
|
|
14299
|
-
|
|
14417
|
+
const item = q.peek(i);
|
|
14418
|
+
cards.push({
|
|
14419
|
+
cardID: item.cardID,
|
|
14420
|
+
status: item.status,
|
|
14421
|
+
origin: isReview(item) ? "review" : "new",
|
|
14422
|
+
score: item.score
|
|
14423
|
+
});
|
|
14300
14424
|
}
|
|
14301
14425
|
return { length: q.length, dequeueCount: q.dequeueCount, cards };
|
|
14302
14426
|
};
|
|
@@ -14319,9 +14443,9 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14319
14443
|
sessionHints: this._sessionHints,
|
|
14320
14444
|
replanActive: this._replanPromise !== null,
|
|
14321
14445
|
replanLabel: this._activeReplanLabel,
|
|
14322
|
-
|
|
14323
|
-
newQ: describe(this.newQ),
|
|
14446
|
+
supplyQ: describe(this.supplyQ),
|
|
14324
14447
|
failedQ: describe(this.failedQ),
|
|
14448
|
+
reviewBacklog: getSrsBacklogDebug(),
|
|
14325
14449
|
drawnCards
|
|
14326
14450
|
};
|
|
14327
14451
|
}
|
|
@@ -14441,7 +14565,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14441
14565
|
*/
|
|
14442
14566
|
static WELL_INDICATED_SCORE = 0.1;
|
|
14443
14567
|
/**
|
|
14444
|
-
*
|
|
14568
|
+
* supplyQ length at or below which the opportunistic depletion-prefetch
|
|
14445
14569
|
* fires. Sets the lead time available for the background replan to land
|
|
14446
14570
|
* before the user actually empties the queue and falls into the
|
|
14447
14571
|
* (synchronous) wedge-breaker path.
|
|
@@ -14454,7 +14578,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14454
14578
|
*/
|
|
14455
14579
|
static DEPLETION_PREFETCH_THRESHOLD = 3;
|
|
14456
14580
|
/**
|
|
14457
|
-
* Internal replan execution. Runs the pipeline,
|
|
14581
|
+
* Internal replan execution. Runs the pipeline, rebuilds the supplyQ,
|
|
14458
14582
|
* atomically swaps it in, and triggers hydration for the new contents.
|
|
14459
14583
|
*
|
|
14460
14584
|
* If the initial replan produces fewer than MIN_WELL_INDICATED cards that
|
|
@@ -14483,8 +14607,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14483
14607
|
}
|
|
14484
14608
|
await this.hydrationService.ensureHydratedCards();
|
|
14485
14609
|
const labelTag = opts.label ? ` [${opts.label}]` : "";
|
|
14486
|
-
this.log(`Replan complete${labelTag}:
|
|
14487
|
-
snapshotQueues(this.
|
|
14610
|
+
this.log(`Replan complete${labelTag}: supplyQ now has ${this.supplyQ.length} cards (mode=${mode})`);
|
|
14611
|
+
snapshotQueues(this.supplyQ.length, this.failedQ.length);
|
|
14488
14612
|
}
|
|
14489
14613
|
addTime(seconds) {
|
|
14490
14614
|
this.endTime = new Date(this.endTime.valueOf() + 1e3 * seconds);
|
|
@@ -14493,10 +14617,10 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14493
14617
|
return this.failedQ.length;
|
|
14494
14618
|
}
|
|
14495
14619
|
toString() {
|
|
14496
|
-
return `Session: ${this.
|
|
14620
|
+
return `Session: ${this.supplyQ.length} supply, ${this.failedQ.length} failed`;
|
|
14497
14621
|
}
|
|
14498
14622
|
reportString() {
|
|
14499
|
-
return `${this.
|
|
14623
|
+
return `${this.supplyQ.dequeueCount} supply, ${this.failedQ.dequeueCount} failed`;
|
|
14500
14624
|
}
|
|
14501
14625
|
/**
|
|
14502
14626
|
* Returns debug information about the current session state.
|
|
@@ -14513,7 +14637,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14513
14637
|
items.push({
|
|
14514
14638
|
courseID: item.courseID || "unknown",
|
|
14515
14639
|
cardID: item.cardID || "unknown",
|
|
14516
|
-
status: item.status || "unknown"
|
|
14640
|
+
status: item.status || "unknown",
|
|
14641
|
+
score: item.score
|
|
14517
14642
|
});
|
|
14518
14643
|
}
|
|
14519
14644
|
return items;
|
|
@@ -14523,15 +14648,10 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14523
14648
|
mode: supportsWeightedCards ? "weighted" : "legacy",
|
|
14524
14649
|
description: supportsWeightedCards ? "Using getWeightedCards() API with scored candidates" : "ERROR: getWeightedCards() not a function."
|
|
14525
14650
|
},
|
|
14526
|
-
|
|
14527
|
-
length: this.
|
|
14528
|
-
dequeueCount: this.
|
|
14529
|
-
items: extractQueueItems(this.
|
|
14530
|
-
},
|
|
14531
|
-
newQueue: {
|
|
14532
|
-
length: this.newQ.length,
|
|
14533
|
-
dequeueCount: this.newQ.dequeueCount,
|
|
14534
|
-
items: extractQueueItems(this.newQ)
|
|
14651
|
+
supplyQueue: {
|
|
14652
|
+
length: this.supplyQ.length,
|
|
14653
|
+
dequeueCount: this.supplyQ.dequeueCount,
|
|
14654
|
+
items: extractQueueItems(this.supplyQ)
|
|
14535
14655
|
},
|
|
14536
14656
|
failedQueue: {
|
|
14537
14657
|
length: this.failedQ.length,
|
|
@@ -14551,30 +14671,29 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14551
14671
|
};
|
|
14552
14672
|
}
|
|
14553
14673
|
/**
|
|
14554
|
-
* Fetch content
|
|
14674
|
+
* Fetch weighted content from all sources, mix across sources, and populate
|
|
14675
|
+
* the single supply queue in pipeline rank order.
|
|
14555
14676
|
*
|
|
14556
|
-
*
|
|
14557
|
-
* 1.
|
|
14558
|
-
*
|
|
14559
|
-
*
|
|
14560
|
-
*
|
|
14561
|
-
|
|
14562
|
-
/**
|
|
14563
|
-
* Fetch weighted content from all sources and populate session queues.
|
|
14677
|
+
* Reviews and new cards compete on one cross-comparable scale (SRS 0.5–1.0
|
|
14678
|
+
* w/ backlog pressure vs ELO 0.0–1.0) — there is no origin split and no
|
|
14679
|
+
* second mixer. The working set is `supplyLimit` cards (the top of the mixed
|
|
14680
|
+
* ranking, plus any `+INF` required cards floated to the front); replans
|
|
14681
|
+
* re-pull and re-rank the whole supply, so a heavy review backlog surfaces as
|
|
14682
|
+
* a refreshed top-ranked working set rather than a frozen 200-card snapshot.
|
|
14564
14683
|
*
|
|
14565
14684
|
* @param options.replan - If true, this is a mid-session replan rather than
|
|
14566
|
-
* initial session setup.
|
|
14567
|
-
*
|
|
14568
|
-
* @param options.additive - If true (replan only), merge
|
|
14569
|
-
* candidates into the front of the existing
|
|
14685
|
+
* initial session setup. Atomically replaces supplyQ contents and treats
|
|
14686
|
+
* empty results as non-fatal.
|
|
14687
|
+
* @param options.additive - If true (replan only), merge high-quality
|
|
14688
|
+
* candidates into the front of the existing supplyQ instead of replacing it.
|
|
14570
14689
|
* @returns Number of "well-indicated" cards (passed all hierarchy filters)
|
|
14571
14690
|
* in the new content. Returns -1 if no content was loaded.
|
|
14572
14691
|
*/
|
|
14573
14692
|
async getWeightedContent(options) {
|
|
14574
14693
|
const replan = options?.replan ?? false;
|
|
14575
14694
|
const additive = options?.additive ?? false;
|
|
14576
|
-
const
|
|
14577
|
-
const fetchLimit =
|
|
14695
|
+
const supplyLimit = options?.limit ?? this._defaultBatchLimit;
|
|
14696
|
+
const fetchLimit = supplyLimit;
|
|
14578
14697
|
if (!replan) {
|
|
14579
14698
|
this._applyHintsToSources();
|
|
14580
14699
|
}
|
|
@@ -14596,7 +14715,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14596
14715
|
}
|
|
14597
14716
|
if (batches.length === 0) {
|
|
14598
14717
|
if (replan) {
|
|
14599
|
-
this.log("Replan: no content from any source, keeping existing
|
|
14718
|
+
this.log("Replan: no content from any source, keeping existing supplyQ");
|
|
14600
14719
|
return -1;
|
|
14601
14720
|
}
|
|
14602
14721
|
throw new Error(
|
|
@@ -14630,64 +14749,59 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14630
14749
|
quotaPerSource,
|
|
14631
14750
|
mixedWeighted
|
|
14632
14751
|
);
|
|
14633
|
-
const
|
|
14634
|
-
const
|
|
14635
|
-
|
|
14636
|
-
|
|
14637
|
-
const mandatoryWeighted = newCandidates.filter((w) => w.score === Number.POSITIVE_INFINITY);
|
|
14638
|
-
const optionalWeighted = newCandidates.filter((w) => w.score !== Number.POSITIVE_INFINITY);
|
|
14639
|
-
const newWeighted = [
|
|
14752
|
+
const candidates = mixedWeighted.filter((w) => !this._servedCardIds.has(w.cardId));
|
|
14753
|
+
const mandatoryWeighted = candidates.filter((w) => w.score === Number.POSITIVE_INFINITY);
|
|
14754
|
+
const optionalWeighted = candidates.filter((w) => w.score !== Number.POSITIVE_INFINITY);
|
|
14755
|
+
const supplyWeighted = [
|
|
14640
14756
|
...mandatoryWeighted,
|
|
14641
|
-
...optionalWeighted.slice(0, Math.max(0,
|
|
14757
|
+
...optionalWeighted.slice(0, Math.max(0, supplyLimit - mandatoryWeighted.length))
|
|
14642
14758
|
];
|
|
14643
14759
|
const mandatoryIds = new Set(mandatoryWeighted.map((w) => w.cardId));
|
|
14644
|
-
|
|
14645
|
-
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
14646
|
-
if (!replan) {
|
|
14647
|
-
for (const w of reviewWeighted) {
|
|
14648
|
-
const reviewItem = {
|
|
14649
|
-
cardID: w.cardId,
|
|
14650
|
-
courseID: w.courseId,
|
|
14651
|
-
contentSourceType: "course",
|
|
14652
|
-
contentSourceID: w.courseId,
|
|
14653
|
-
reviewID: w.reviewID,
|
|
14654
|
-
status: "review"
|
|
14655
|
-
};
|
|
14656
|
-
this.reviewQ.add(reviewItem, reviewItem.cardID);
|
|
14657
|
-
report += `Review: ${w.courseId}::${w.cardId} (score: ${w.score.toFixed(2)})
|
|
14658
|
-
`;
|
|
14659
|
-
}
|
|
14660
|
-
}
|
|
14661
|
-
const wellIndicated = newWeighted.filter(
|
|
14760
|
+
const wellIndicated = supplyWeighted.filter(
|
|
14662
14761
|
(w) => w.score >= _SessionController.WELL_INDICATED_SCORE
|
|
14663
14762
|
).length;
|
|
14664
|
-
|
|
14665
|
-
|
|
14666
|
-
const
|
|
14667
|
-
|
|
14668
|
-
|
|
14669
|
-
contentSourceType: "course",
|
|
14670
|
-
contentSourceID: w.courseId,
|
|
14671
|
-
status: "new"
|
|
14672
|
-
};
|
|
14673
|
-
newItems.push(newItem);
|
|
14674
|
-
report += `New: ${w.courseId}::${w.cardId} (score: ${w.score.toFixed(2)})
|
|
14763
|
+
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
14764
|
+
const supplyItems = supplyWeighted.map((w) => {
|
|
14765
|
+
const origin = getCardOrigin(w);
|
|
14766
|
+
const scoreStr = Number.isFinite(w.score) ? w.score.toFixed(2) : "+INF";
|
|
14767
|
+
report += `${origin === "review" ? "Review" : "New"}: ${w.courseId}::${w.cardId} (score: ${scoreStr})
|
|
14675
14768
|
`;
|
|
14676
|
-
|
|
14769
|
+
return this._buildSupplyItem(w, origin);
|
|
14770
|
+
});
|
|
14677
14771
|
if (additive) {
|
|
14678
|
-
const added = this.
|
|
14679
|
-
report += `Additive merge: ${added}
|
|
14772
|
+
const added = this.supplyQ.mergeToFront(supplyItems, (item) => item.cardID, mandatoryIds);
|
|
14773
|
+
report += `Additive merge: ${added} cards added to front of supplyQ
|
|
14680
14774
|
`;
|
|
14681
14775
|
} else if (replan) {
|
|
14682
|
-
this.
|
|
14776
|
+
this.supplyQ.replaceAll(supplyItems, (item) => item.cardID);
|
|
14683
14777
|
} else {
|
|
14684
|
-
for (const item of
|
|
14685
|
-
this.
|
|
14778
|
+
for (const item of supplyItems) {
|
|
14779
|
+
this.supplyQ.add(item, item.cardID);
|
|
14686
14780
|
}
|
|
14687
14781
|
}
|
|
14688
14782
|
this.log(report);
|
|
14689
14783
|
return wellIndicated;
|
|
14690
14784
|
}
|
|
14785
|
+
/**
|
|
14786
|
+
* Build a supply item from a weighted candidate. Review-origin cards carry
|
|
14787
|
+
* their `reviewID` so SRS outcome tracking and re-presentation work; new
|
|
14788
|
+
* cards do not. `score` is carried on both for the debug overlay.
|
|
14789
|
+
*/
|
|
14790
|
+
_buildSupplyItem(w, origin = getCardOrigin(w)) {
|
|
14791
|
+
const base = {
|
|
14792
|
+
cardID: w.cardId,
|
|
14793
|
+
courseID: w.courseId,
|
|
14794
|
+
contentSourceType: "course",
|
|
14795
|
+
contentSourceID: w.courseId,
|
|
14796
|
+
score: w.score
|
|
14797
|
+
};
|
|
14798
|
+
if (origin === "review") {
|
|
14799
|
+
const reviewItem = { ...base, status: "review", reviewID: w.reviewID };
|
|
14800
|
+
return reviewItem;
|
|
14801
|
+
}
|
|
14802
|
+
const newItem = { ...base, status: "new" };
|
|
14803
|
+
return newItem;
|
|
14804
|
+
}
|
|
14691
14805
|
/**
|
|
14692
14806
|
* Returns items that should be pre-hydrated.
|
|
14693
14807
|
* Deterministic: top N items from each queue to ensure coverage.
|
|
@@ -14695,71 +14809,73 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14695
14809
|
*/
|
|
14696
14810
|
_getItemsToHydrate() {
|
|
14697
14811
|
const items = [];
|
|
14698
|
-
const
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
for (let i = 0; i < Math.min(ITEMS_PER_QUEUE, this.newQ.length); i++) {
|
|
14703
|
-
items.push(this.newQ.peek(i));
|
|
14812
|
+
const SUPPLY_PREFETCH = 3;
|
|
14813
|
+
const FAILED_PREFETCH = 2;
|
|
14814
|
+
for (let i = 0; i < Math.min(SUPPLY_PREFETCH, this.supplyQ.length); i++) {
|
|
14815
|
+
items.push(this.supplyQ.peek(i));
|
|
14704
14816
|
}
|
|
14705
|
-
for (let i = 0; i < Math.min(
|
|
14817
|
+
for (let i = 0; i < Math.min(FAILED_PREFETCH, this.failedQ.length); i++) {
|
|
14706
14818
|
items.push(this.failedQ.peek(i));
|
|
14707
14819
|
}
|
|
14708
14820
|
return items;
|
|
14709
14821
|
}
|
|
14710
14822
|
/**
|
|
14711
14823
|
* Selects the next item to present to the user.
|
|
14712
|
-
*
|
|
14824
|
+
*
|
|
14825
|
+
* The supplyQ is already rank-ordered (the pipeline + mixer did the mixing,
|
|
14826
|
+
* with `+INF` required cards floated to the front), so the primary path is a
|
|
14827
|
+
* deterministic front-to-back draw — no second new-vs-review mixer. The only
|
|
14828
|
+
* remaining decisions are (a) when the session ends and (b) when to interleave
|
|
14829
|
+
* a remediation card from failedQ. See decision doc §2/§3/§7.
|
|
14713
14830
|
*/
|
|
14714
14831
|
_selectNextItemToHydrate() {
|
|
14715
|
-
|
|
14716
|
-
let newBound = 0.1;
|
|
14717
|
-
let reviewBound = 0.75;
|
|
14718
|
-
if (this.reviewQ.length === 0 && this.failedQ.length === 0 && this.newQ.length === 0) {
|
|
14832
|
+
if (this.supplyQ.length === 0 && this.failedQ.length === 0) {
|
|
14719
14833
|
return null;
|
|
14720
14834
|
}
|
|
14721
14835
|
if (this._secondsRemaining < 2 && this.failedQ.length === 0 && this._minCardsGuarantee <= 0) {
|
|
14722
14836
|
return null;
|
|
14723
14837
|
}
|
|
14724
14838
|
if (this._secondsRemaining <= 0 && this._minCardsGuarantee <= 0) {
|
|
14725
|
-
|
|
14726
|
-
return this.failedQ.peek(0);
|
|
14727
|
-
} else {
|
|
14728
|
-
return null;
|
|
14729
|
-
}
|
|
14730
|
-
}
|
|
14731
|
-
if (this.newQ.dequeueCount < this.sources.length && this.newQ.length) {
|
|
14732
|
-
return this.newQ.peek(0);
|
|
14839
|
+
return this.failedQ.length > 0 ? this.failedQ.peek(0) : null;
|
|
14733
14840
|
}
|
|
14734
|
-
const
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
if (availableTime > 20) {
|
|
14738
|
-
newBound = 0.5;
|
|
14739
|
-
reviewBound = 0.9;
|
|
14740
|
-
} else if (this._secondsRemaining - cleanupTime > 20) {
|
|
14741
|
-
newBound = 0.05;
|
|
14742
|
-
reviewBound = 0.9;
|
|
14743
|
-
} else {
|
|
14744
|
-
newBound = 0.01;
|
|
14745
|
-
reviewBound = 0.1;
|
|
14841
|
+
const supplyTop = this.supplyQ.length > 0 ? this.supplyQ.peek(0) : null;
|
|
14842
|
+
if (this._minCardsGuarantee > 0 && supplyTop) {
|
|
14843
|
+
return supplyTop;
|
|
14746
14844
|
}
|
|
14747
|
-
if (this.failedQ.length
|
|
14748
|
-
|
|
14845
|
+
if (this.failedQ.length > 0 && this._shouldInterleaveFailed(supplyTop !== null)) {
|
|
14846
|
+
return this.failedQ.peek(0);
|
|
14749
14847
|
}
|
|
14750
|
-
if (
|
|
14751
|
-
|
|
14848
|
+
if (supplyTop) {
|
|
14849
|
+
return supplyTop;
|
|
14752
14850
|
}
|
|
14753
|
-
if (
|
|
14754
|
-
return this.newQ.peek(0);
|
|
14755
|
-
} else if (choice < reviewBound && this.reviewQ.length) {
|
|
14756
|
-
return this.reviewQ.peek(0);
|
|
14757
|
-
} else if (this.failedQ.length) {
|
|
14851
|
+
if (this.failedQ.length > 0) {
|
|
14758
14852
|
return this.failedQ.peek(0);
|
|
14759
|
-
} else {
|
|
14760
|
-
this.log(`No more cards available for the session!`);
|
|
14761
|
-
return null;
|
|
14762
14853
|
}
|
|
14854
|
+
this.log(`No more cards available for the session!`);
|
|
14855
|
+
return null;
|
|
14856
|
+
}
|
|
14857
|
+
/** Supply draws between forced failed-queue interleaves (light steady cadence). */
|
|
14858
|
+
static FAILED_INTERLEAVE_EVERY = 4;
|
|
14859
|
+
/**
|
|
14860
|
+
* Slack (seconds) below which the endgame failed-pressure kicks in: when the
|
|
14861
|
+
* time left after clearing remediation drops under this, bias hard to failed
|
|
14862
|
+
* so the session doesn't end with un-cleared remediation. Mirrors the old
|
|
14863
|
+
* `availableTime > 20` ladder thresholds.
|
|
14864
|
+
*/
|
|
14865
|
+
static FAILED_ENDGAME_SLACK_SECONDS = 20;
|
|
14866
|
+
/**
|
|
14867
|
+
* Whether to interleave a failed (remediation) card now instead of drawing
|
|
14868
|
+
* the supply head. Replaces the old `newBound`/`reviewBound` probability
|
|
14869
|
+
* ladder's failed path (decision doc §7).
|
|
14870
|
+
*
|
|
14871
|
+
* @param supplyAvailable - whether supplyQ has a card to draw instead.
|
|
14872
|
+
*/
|
|
14873
|
+
_shouldInterleaveFailed(supplyAvailable) {
|
|
14874
|
+
if (this.failedQ.length === 0) return false;
|
|
14875
|
+
if (!supplyAvailable) return true;
|
|
14876
|
+
const availableTime = this._secondsRemaining - this.estimateCleanupTime();
|
|
14877
|
+
if (availableTime <= _SessionController.FAILED_ENDGAME_SLACK_SECONDS) return true;
|
|
14878
|
+
return this._supplyDrawsSinceFailed >= _SessionController.FAILED_INTERLEAVE_EVERY;
|
|
14763
14879
|
}
|
|
14764
14880
|
async nextCard(action = "dismiss-success") {
|
|
14765
14881
|
this.dismissCurrentCard(action);
|
|
@@ -14767,22 +14883,21 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14767
14883
|
this._minCardsGuarantee--;
|
|
14768
14884
|
this.log(`[CardGuarantee] ${this._minCardsGuarantee} guaranteed cards remaining`);
|
|
14769
14885
|
}
|
|
14770
|
-
if (this._replanPromise && this.
|
|
14886
|
+
if (this._replanPromise && this.supplyQ.length === 0 && this.failedQ.length === 0) {
|
|
14771
14887
|
this.log("nextCard: queues empty, awaiting in-flight replan before drawing");
|
|
14772
14888
|
await this._replanPromise;
|
|
14773
14889
|
}
|
|
14774
|
-
if (this.
|
|
14890
|
+
if (this.supplyQ.length <= _SessionController.DEPLETION_PREFETCH_THRESHOLD && this._secondsRemaining > 0 && !this._replanPromise) {
|
|
14775
14891
|
this._suppressQualityReplan = false;
|
|
14776
|
-
const otherContent = this.reviewQ.length + this.failedQ.length;
|
|
14777
14892
|
this.log(
|
|
14778
|
-
`[AutoReplan:depletion]
|
|
14893
|
+
`[AutoReplan:depletion] supplyQ has ${this.supplyQ.length} card(s) (${this.failedQ.length} failed pending) with ${this._secondsRemaining}s remaining. Triggering background replan.`
|
|
14779
14894
|
);
|
|
14780
14895
|
void this.requestReplan({ label: "auto:depletion", mode: "merge" });
|
|
14781
14896
|
}
|
|
14782
14897
|
const REPLAN_BUFFER = 3;
|
|
14783
|
-
if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.
|
|
14898
|
+
if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.supplyQ.length > 0 && !this._replanPromise) {
|
|
14784
14899
|
this.log(
|
|
14785
|
-
`[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (
|
|
14900
|
+
`[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (supplyQ: ${this.supplyQ.length}). Triggering background replan.`
|
|
14786
14901
|
);
|
|
14787
14902
|
void this.requestReplan({ label: "auto:quality" });
|
|
14788
14903
|
}
|
|
@@ -14794,12 +14909,12 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14794
14909
|
const WEDGE_MAX_EMPTY_STREAK = 3;
|
|
14795
14910
|
const WEDGE_BACKOFF_MS = 250;
|
|
14796
14911
|
let wedgeEmptyStreak = 0;
|
|
14797
|
-
while (this._secondsRemaining > 0 && this.
|
|
14912
|
+
while (this._secondsRemaining > 0 && this.supplyQ.length === 0 && this.failedQ.length === 0) {
|
|
14798
14913
|
this.log(
|
|
14799
14914
|
`[WedgeBreaker] All queues empty with ${this._secondsRemaining}s remaining. Running pipeline (attempt ${wedgeEmptyStreak + 1}/${WEDGE_MAX_EMPTY_STREAK}).`
|
|
14800
14915
|
);
|
|
14801
14916
|
await this._replanUncoalesced({ label: "wedge-breaker" });
|
|
14802
|
-
if (this.
|
|
14917
|
+
if (this.supplyQ.length === 0 && this.failedQ.length === 0) {
|
|
14803
14918
|
wedgeEmptyStreak++;
|
|
14804
14919
|
if (wedgeEmptyStreak >= WEDGE_MAX_EMPTY_STREAK) {
|
|
14805
14920
|
this.log(
|
|
@@ -14829,15 +14944,16 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14829
14944
|
await this.hydrationService.ensureHydratedCards();
|
|
14830
14945
|
this._currentCard = card;
|
|
14831
14946
|
const origin = nextItem.status === "review" || nextItem.status === "failed-review" ? "review" : nextItem.status === "new" || nextItem.status === "failed-new" ? "new" : "failed";
|
|
14832
|
-
const queueSource = nextItem.status.startsWith("failed") ? "failedQ" :
|
|
14947
|
+
const queueSource = nextItem.status.startsWith("failed") ? "failedQ" : "supplyQ";
|
|
14833
14948
|
recordCardPresentation(
|
|
14834
14949
|
nextItem.cardID,
|
|
14835
14950
|
nextItem.courseID,
|
|
14836
14951
|
this.courseNameCache.get(nextItem.courseID),
|
|
14837
14952
|
origin,
|
|
14838
|
-
queueSource
|
|
14953
|
+
queueSource,
|
|
14954
|
+
nextItem.score
|
|
14839
14955
|
);
|
|
14840
|
-
snapshotQueues(this.
|
|
14956
|
+
snapshotQueues(this.supplyQ.length, this.failedQ.length);
|
|
14841
14957
|
return card;
|
|
14842
14958
|
}
|
|
14843
14959
|
this.log(`Skipping card ${nextItem.cardID}: hydration failed, trying next`);
|
|
@@ -14907,6 +15023,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14907
15023
|
};
|
|
14908
15024
|
}
|
|
14909
15025
|
this.failedQ.add(failedItem, failedItem.cardID);
|
|
15026
|
+
this._supplyDrawsSinceFailed = 0;
|
|
14910
15027
|
} else if (action === "dismiss-error") {
|
|
14911
15028
|
this.hydrationService.removeCard(this._currentCard.item.cardID);
|
|
14912
15029
|
} else if (action === "dismiss-failed") {
|
|
@@ -14920,15 +15037,15 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14920
15037
|
removeItemFromQueue(item) {
|
|
14921
15038
|
this._clearDurableRequirement(item.cardID);
|
|
14922
15039
|
this._servedCardIds.add(item.cardID);
|
|
14923
|
-
if (this.
|
|
14924
|
-
this.
|
|
14925
|
-
|
|
14926
|
-
|
|
15040
|
+
if (this.failedQ.peek(0)?.cardID === item.cardID) {
|
|
15041
|
+
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
15042
|
+
this._supplyDrawsSinceFailed = 0;
|
|
15043
|
+
} else if (this.supplyQ.peek(0)?.cardID === item.cardID) {
|
|
15044
|
+
this.supplyQ.dequeue((queueItem) => queueItem.cardID);
|
|
15045
|
+
this._supplyDrawsSinceFailed++;
|
|
14927
15046
|
if (this._wellIndicatedRemaining > 0) {
|
|
14928
15047
|
this._wellIndicatedRemaining--;
|
|
14929
15048
|
}
|
|
14930
|
-
} else if (this.failedQ.peek(0)?.cardID === item.cardID) {
|
|
14931
|
-
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
14932
15049
|
}
|
|
14933
15050
|
}
|
|
14934
15051
|
/**
|
|
@@ -15027,6 +15144,7 @@ export {
|
|
|
15027
15144
|
areQuestionRecords,
|
|
15028
15145
|
buildStrategyStateId,
|
|
15029
15146
|
captureMixerRun,
|
|
15147
|
+
clearSrsBacklogDebug,
|
|
15030
15148
|
computeDeviation,
|
|
15031
15149
|
computeEffectiveWeight,
|
|
15032
15150
|
computeOutcomeSignal,
|
|
@@ -15047,6 +15165,7 @@ export {
|
|
|
15047
15165
|
getRegisteredNavigator,
|
|
15048
15166
|
getRegisteredNavigatorNames,
|
|
15049
15167
|
getRegisteredNavigatorRole,
|
|
15168
|
+
getSrsBacklogDebug,
|
|
15050
15169
|
getStudySource,
|
|
15051
15170
|
hasRegisteredNavigator,
|
|
15052
15171
|
importParsedCards,
|