@vue-skuilder/db 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/{contentSource-Cplhv3bJ.d.ts → contentSource-C-0t0y0V.d.ts} +7 -0
  2. package/dist/{contentSource-kI9_jwTu.d.cts → contentSource-jSkcOt2s.d.cts} +7 -0
  3. package/dist/core/index.d.cts +29 -4
  4. package/dist/core/index.d.ts +29 -4
  5. package/dist/core/index.js +132 -39
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +130 -39
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-DrBqOUa3.d.ts → dataLayerProvider-BB0oi9T0.d.ts} +1 -1
  10. package/dist/{dataLayerProvider-CiA2Rr0v.d.cts → dataLayerProvider-BDClIrFC.d.cts} +1 -1
  11. package/dist/impl/couch/index.d.cts +2 -2
  12. package/dist/impl/couch/index.d.ts +2 -2
  13. package/dist/impl/couch/index.js +128 -39
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +128 -39
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.cts +2 -2
  18. package/dist/impl/static/index.d.ts +2 -2
  19. package/dist/impl/static/index.js +128 -39
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +128 -39
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/index.d.cts +115 -81
  24. package/dist/index.d.ts +115 -81
  25. package/dist/index.js +371 -251
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +369 -251
  28. package/dist/index.mjs.map +1 -1
  29. package/docs/navigators-architecture.md +29 -13
  30. package/package.json +3 -3
  31. package/src/core/interfaces/contentSource.ts +7 -0
  32. package/src/core/navigators/SrsDebugger.ts +53 -0
  33. package/src/core/navigators/generators/prescribed.ts +76 -9
  34. package/src/core/navigators/generators/srs.ts +81 -37
  35. package/src/core/navigators/index.ts +5 -0
  36. package/src/study/SessionController.ts +260 -249
  37. package/src/study/SessionDebugger.ts +15 -25
  38. 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 lets the pipeline's
2634
- * scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
2635
- * this method's job; it only guarantees presence.
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: BASE_PRACTICE_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: BASE_PRACTICE_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, MAX_BACKLOG_PRESSURE, SRSNavigator;
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
- MAX_BACKLOG_PRESSURE = 0.5;
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 backlogPressure = this.computeBacklogPressure(dueReviews.length);
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 = backlogPressure > 0 ? ` [backlog pressure: +${backlogPressure.toFixed(2)}]` : ` [healthy backlog]`;
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, backlogPressure);
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
- return { cards: scored.sort((a, b) => b.score - a.score).slice(0, limit) };
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
- * Backlog pressure is 0 when at or below healthy threshold,
2979
- * and increases linearly above it, maxing out at MAX_BACKLOG_PRESSURE.
3062
+ * ×1.0 at or below the healthy threshold (no boost), increasing linearly above
3063
+ * it and maxing out at MAX_BACKLOG_MULTIPLIER at the healthy backlog.
2980
3064
  *
2981
- * Examples (with default healthyBacklog=20):
2982
- * - 10 due reviews → 0.00 (healthy)
2983
- * - 20 due reviews → 0.00 (at threshold)
2984
- * - 40 due reviews → 0.25 (2x threshold)
2985
- * - 60 due reviews → 0.50 (3x threshold, maxed)
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 Backlog pressure score to add to urgency (0 to MAX_BACKLOG_PRESSURE)
3072
+ * @returns Multiplier applied to review urgency (1.0 to MAX_BACKLOG_MULTIPLIER)
2989
3073
  */
2990
- computeBacklogPressure(dueCount) {
3074
+ computeBacklogMultiplier(dueCount) {
2991
3075
  if (dueCount <= this.healthyBacklog) {
2992
- return 0;
3076
+ return 1;
2993
3077
  }
2994
3078
  const excess = dueCount - this.healthyBacklog;
2995
- const pressure = excess / this.healthyBacklog * (MAX_BACKLOG_PRESSURE / 2);
2996
- return Math.min(MAX_BACKLOG_PRESSURE, pressure);
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 boost when review backlog exceeds healthy threshold
3012
- * - At healthy backlog: 0
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 + (urgency factors * 0.45) + backlog pressure
3017
- * Result range: 0.5 to 1.0 (uncapped to allow high-urgency reviews to compete with new cards)
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 backlogPressure - Pre-computed backlog pressure (0 to 0.5)
3106
+ * @param backlogMultiplier - Pre-computed backlog multiplier (1.0 to MAX_BACKLOG_MULTIPLIER)
3022
3107
  */
3023
- computeUrgencyScore(review, now, backlogPressure) {
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 = Math.min(1, baseScore + backlogPressure);
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 (backlogPressure > 0) {
3041
- reasonParts.push(`backlog: +${backlogPressure.toFixed(2)}`);
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();
@@ -10477,6 +10566,7 @@ __export(index_exports, {
10477
10566
  areQuestionRecords: () => areQuestionRecords,
10478
10567
  buildStrategyStateId: () => buildStrategyStateId,
10479
10568
  captureMixerRun: () => captureMixerRun,
10569
+ clearSrsBacklogDebug: () => clearSrsBacklogDebug,
10480
10570
  computeDeviation: () => computeDeviation,
10481
10571
  computeEffectiveWeight: () => computeEffectiveWeight,
10482
10572
  computeOutcomeSignal: () => computeOutcomeSignal,
@@ -10497,6 +10587,7 @@ __export(index_exports, {
10497
10587
  getRegisteredNavigator: () => getRegisteredNavigator,
10498
10588
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
10499
10589
  getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
10590
+ getSrsBacklogDebug: () => getSrsBacklogDebug,
10500
10591
  getStudySource: () => getStudySource,
10501
10592
  hasRegisteredNavigator: () => hasRegisteredNavigator,
10502
10593
  importParsedCards: () => importParsedCards,
@@ -13444,6 +13535,7 @@ mountMixerDebugger();
13444
13535
  // src/study/SessionDebugger.ts
13445
13536
  init_logger();
13446
13537
  init_PipelineDebugger();
13538
+ init_SrsDebugger();
13447
13539
 
13448
13540
  // src/study/SessionOverlay.ts
13449
13541
  init_logger();
@@ -13465,8 +13557,7 @@ var lastSnapshot = null;
13465
13557
  var copyFlashUntil = 0;
13466
13558
  var minified = false;
13467
13559
  var expanded = {
13468
- reviewQ: false,
13469
- newQ: false,
13560
+ supplyQ: false,
13470
13561
  failedQ: false,
13471
13562
  drawn: false
13472
13563
  };
@@ -13535,7 +13626,7 @@ function render() {
13535
13626
  attachHandlers();
13536
13627
  return;
13537
13628
  }
13538
- overlayEl.innerHTML = headerHtml() + replanHtml(s) + metaHtml(s) + hintsHtml(s.sessionHints) + queueHtml("reviewQ", "reviewQ", s.reviewQ) + queueHtml("newQ", "newQ", s.newQ) + queueHtml("failedQ", "failedQ", s.failedQ) + drawnHtml("drawn", s.drawnCards);
13629
+ 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
13630
  attachHandlers();
13540
13631
  }
13541
13632
  function attachHandlers() {
@@ -13627,6 +13718,29 @@ function hintsHtml(h) {
13627
13718
  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
13719
  return `<div style="margin-bottom:6px"><div style="color:#86efac">sessionHints</div>${body}</div>`;
13629
13720
  }
13721
+ function backlogHtml(backlog) {
13722
+ if (!backlog.length) return "";
13723
+ const rows = backlog.map((b) => {
13724
+ const maxed = b.backlogMultiplier >= b.maxBacklogMultiplier - 1e-9;
13725
+ const multColor = b.backlogMultiplier <= 1 ? "#86efac" : maxed ? "#fca5a5" : "#fcd34d";
13726
+ const headroom = maxed ? "maxed \u2014 boosts decide order until they relax" : b.backlogMultiplier > 1 ? "climbing as backlog grows" : "healthy \u2014 no pressure";
13727
+ const top = b.topReviewScore !== null ? b.topReviewScore.toFixed(2) : "\u2014";
13728
+ const next = b.nextDueIn ? ` <span style="opacity:.6">\xB7 next due ${esc(b.nextDueIn)}</span>` : "";
13729
+ 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>`;
13730
+ }).join("");
13731
+ return `<div style="margin-bottom:6px"><div style="color:#93c5fd">review backpressure</div>${rows}</div>`;
13732
+ }
13733
+ function fmtScore(score) {
13734
+ if (score === void 0) return "";
13735
+ if (!Number.isFinite(score)) return "REQ";
13736
+ return score.toFixed(2);
13737
+ }
13738
+ function queueItemHtml(item) {
13739
+ const tagColor = item.origin === "review" ? "#93c5fd" : "#fcd34d";
13740
+ const score = fmtScore(item.score);
13741
+ const label = `${item.origin === "review" ? "r" : "n"}${score ? " " + score : ""}`;
13742
+ return `${esc(item.cardID)}<span style="color:${tagColor};opacity:.85"> [${label}]</span>`;
13743
+ }
13630
13744
  function queueHtml(key, label, q) {
13631
13745
  const collapsible = q.length > INLINE_THRESHOLD;
13632
13746
  const isOpen = collapsible && expanded[key];
@@ -13641,7 +13755,7 @@ function queueHtml(key, label, q) {
13641
13755
  const shown = isOpen ? q.cards : q.cards.slice(0, INLINE_THRESHOLD);
13642
13756
  const hiddenCount = q.length - shown.length;
13643
13757
  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">${esc(c)}</li>`).join("") + `</ol>`;
13758
+ 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
13759
  if (collapsible) {
13646
13760
  const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
13647
13761
  body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
@@ -13704,13 +13818,29 @@ function snapshotToText(s) {
13704
13818
  if (h.excludeCards?.length) hintParts.push(` excludeCards: ${h.excludeCards.join(", ")}`);
13705
13819
  }
13706
13820
  lines.push(hintParts.length ? hintParts.join("\n") : " none");
13821
+ if (s.reviewBacklog.length) {
13822
+ lines.push("");
13823
+ lines.push("review backpressure:");
13824
+ for (const b of s.reviewBacklog) {
13825
+ const maxed = b.backlogMultiplier >= b.maxBacklogMultiplier - 1e-9;
13826
+ const headroom = maxed ? "maxed (boosts decide order)" : b.backlogMultiplier > 1 ? "climbing" : "healthy";
13827
+ const top = b.topReviewScore !== null ? b.topReviewScore.toFixed(2) : "\u2014";
13828
+ const next = b.nextDueIn ? `, next due ${b.nextDueIn}` : "";
13829
+ lines.push(
13830
+ ` ${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}`
13831
+ );
13832
+ }
13833
+ }
13707
13834
  const queueText = (label, q) => {
13708
13835
  lines.push("");
13709
13836
  lines.push(`${label}: ${q.length} (drawn ${q.dequeueCount})`);
13710
- q.cards.forEach((c, i) => lines.push(` ${i + 1}. ${c}`));
13837
+ q.cards.forEach((c, i) => {
13838
+ const score = fmtScore(c.score);
13839
+ const tag = `${c.origin === "review" ? "r" : "n"}${score ? " " + score : ""}`;
13840
+ lines.push(` ${i + 1}. ${c.cardID} [${tag}]`);
13841
+ });
13711
13842
  };
13712
- queueText("reviewQ", s.reviewQ);
13713
- queueText("newQ", s.newQ);
13843
+ queueText("supplyQ", s.supplyQ);
13714
13844
  queueText("failedQ", s.failedQ);
13715
13845
  lines.push("");
13716
13846
  lines.push(`drawn: ${s.drawnCards.length}`);
@@ -13735,16 +13865,16 @@ function esc(value) {
13735
13865
  var activeSession = null;
13736
13866
  var sessionHistory = [];
13737
13867
  var MAX_HISTORY = 5;
13738
- function startSessionTracking(reviewQLength, newQLength, failedQLength) {
13868
+ function startSessionTracking(supplyQLength, failedQLength) {
13739
13869
  clearRunHistory();
13870
+ clearSrsBacklogDebug();
13740
13871
  const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
13741
13872
  activeSession = {
13742
13873
  sessionId,
13743
13874
  startTime: /* @__PURE__ */ new Date(),
13744
13875
  initialQueues: {
13745
13876
  timestamp: /* @__PURE__ */ new Date(),
13746
- reviewQLength,
13747
- newQLength,
13877
+ supplyQLength,
13748
13878
  failedQLength
13749
13879
  },
13750
13880
  presentations: [],
@@ -13768,17 +13898,15 @@ function recordCardPresentation(cardId, courseId, courseName, origin, queueSourc
13768
13898
  score
13769
13899
  });
13770
13900
  }
13771
- function snapshotQueues(reviewQLength, newQLength, failedQLength, reviewQNext3, newQNext3) {
13901
+ function snapshotQueues(supplyQLength, failedQLength, supplyQNext3) {
13772
13902
  if (!activeSession) {
13773
13903
  return;
13774
13904
  }
13775
13905
  activeSession.queueSnapshots.push({
13776
13906
  timestamp: /* @__PURE__ */ new Date(),
13777
- reviewQLength,
13778
- newQLength,
13907
+ supplyQLength,
13779
13908
  failedQLength,
13780
- reviewQNext3,
13781
- newQNext3
13909
+ supplyQNext3
13782
13910
  });
13783
13911
  }
13784
13912
  function endSessionTracking() {
@@ -13800,13 +13928,9 @@ function showCurrentQueue() {
13800
13928
  }
13801
13929
  const latest = activeSession.queueSnapshots[activeSession.queueSnapshots.length - 1] || activeSession.initialQueues;
13802
13930
  console.group("\u{1F4CA} Current Queue State");
13803
- logger.info(`Review Queue: ${latest.reviewQLength} cards`);
13804
- if (latest.reviewQNext3 && latest.reviewQNext3.length > 0) {
13805
- logger.info(` Next: ${latest.reviewQNext3.join(", ")}`);
13806
- }
13807
- logger.info(`New Queue: ${latest.newQLength} cards`);
13808
- if (latest.newQNext3 && latest.newQNext3.length > 0) {
13809
- logger.info(` Next: ${latest.newQNext3.join(", ")}`);
13931
+ logger.info(`Supply Queue: ${latest.supplyQLength} cards`);
13932
+ if (latest.supplyQNext3 && latest.supplyQNext3.length > 0) {
13933
+ logger.info(` Next: ${latest.supplyQNext3.join(", ")}`);
13810
13934
  }
13811
13935
  logger.info(`Failed Queue: ${latest.failedQLength} cards`);
13812
13936
  console.groupEnd();
@@ -14023,15 +14147,6 @@ var SessionController = class _SessionController extends Loggable {
14023
14147
  * Individual replans can override via `ReplanOptions.limit`.
14024
14148
  */
14025
14149
  _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
14150
  sources;
14036
14151
  // dataLayer and getViewComponent now injected into CardHydrationService
14037
14152
  _sessionRecord = [];
@@ -14040,10 +14155,28 @@ var SessionController = class _SessionController extends Loggable {
14040
14155
  }
14041
14156
  // Session card stores
14042
14157
  _currentCard = null;
14043
- reviewQ = new ItemQueue();
14044
- newQ = new ItemQueue();
14158
+ /**
14159
+ * The single supply queue: `new` + `review` items interleaved in pipeline
14160
+ * rank order (the mixer's score-ordered, source-interleaved output, with
14161
+ * `+INF` required cards floated to the front). Drawn front-to-back; reviews
14162
+ * and new compete on one cross-comparable scale rather than being re-mixed
14163
+ * by a probability gate. Replaced/re-ranked wholesale on replan. See
14164
+ * `docs/decision-single-supply-queue.md`.
14165
+ */
14166
+ supplyQ = new ItemQueue();
14045
14167
  failedQ = new ItemQueue();
14046
14168
  // END Session card stores
14169
+ /**
14170
+ * Supply draws since the last failed-queue *event* (a failed draw, or a card
14171
+ * entering failedQ on failure). Drives the light steady failed-interleave
14172
+ * (§7): after this many consecutive supply draws, a pending failed card is
14173
+ * drawn so remediation doesn't starve mid-session. Incremented on each supply
14174
+ * draw; reset to 0 both when a failed card is drawn AND when one is added to
14175
+ * failedQ — the latter gives a just-failed card spacing instead of an instant
14176
+ * retry (the counter would otherwise already be ≥ threshold from the preceding
14177
+ * supply run).
14178
+ */
14179
+ _supplyDrawsSinceFailed = 0;
14047
14180
  /**
14048
14181
  * Promise tracking a currently in-progress replan, or null if idle.
14049
14182
  * Used by nextCard() to await completion before drawing from queues.
@@ -14057,8 +14190,8 @@ var SessionController = class _SessionController extends Loggable {
14057
14190
  */
14058
14191
  _activeReplanLabel = null;
14059
14192
  /**
14060
- * Number of well-indicated new cards remaining before the queue
14061
- * degrades to poorly-indicated content. Decremented on each newQ
14193
+ * Number of well-indicated supply cards remaining before the queue
14194
+ * degrades to poorly-indicated content. Decremented on each supplyQ
14062
14195
  * draw; when it hits 0, a replan is triggered automatically
14063
14196
  * (user state has changed from completing good cards).
14064
14197
  */
@@ -14067,7 +14200,7 @@ var SessionController = class _SessionController extends Loggable {
14067
14200
  * When true, suppresses the quality-based auto-replan trigger in
14068
14201
  * nextCard(). Set after a burst replan (small limit) to prevent the
14069
14202
  * auto-replan from clobbering the burst cards before they're consumed.
14070
- * Cleared when the depletion-triggered replan fires (newQ exhausted).
14203
+ * Cleared when the depletion-triggered replan fires (supplyQ exhausted).
14071
14204
  */
14072
14205
  _suppressQualityReplan = false;
14073
14206
  /**
@@ -14096,13 +14229,15 @@ var SessionController = class _SessionController extends Loggable {
14096
14229
  * a draw the instant it happens — earlier than `_sessionRecord`, which only
14097
14230
  * lands once the card is *responded to*.
14098
14231
  *
14099
- * Used to keep already-served cards out of newQ on every (re)plan: a `new`
14100
- * card shown once must never re-enter newQ this session. This is the general
14101
- * guard against re-presentation including the case where a replan in flight
14102
- * captured a now-drawn card (e.g. a +INF require-injected follow-up the
14103
- * depletion prefetch grabbed just before it was drawn). Reviews/failed cards
14104
- * legitimately recur and are tracked by their own queues, so this only gates
14105
- * `new`-origin candidates.
14232
+ * Used to keep already-served cards out of supplyQ on every (re)plan, across
14233
+ * ALL origins: a `new` card shown once must never re-enter, and once replans
14234
+ * re-pull reviews, an answered/in-flight review must not re-enter the supply
14235
+ * before its SRS reschedule clears the due-window (the review-loop guard,
14236
+ * decision doc §4). This is the general guard against re-presentation —
14237
+ * including the case where a replan in flight captured a now-drawn card (e.g.
14238
+ * a +INF require-injected follow-up the depletion prefetch grabbed just before
14239
+ * it was drawn). failedQ is separate and controller-owned, so failed cards
14240
+ * legitimately recur there without being gated here.
14106
14241
  */
14107
14242
  _servedCardIds = /* @__PURE__ */ new Set();
14108
14243
  /**
@@ -14127,14 +14262,12 @@ var SessionController = class _SessionController extends Loggable {
14127
14262
  return this._minCardsGuarantee > 0;
14128
14263
  }
14129
14264
  get report() {
14130
- const reviewCount = this.reviewQ.dequeueCount;
14131
- const newCount = this.newQ.dequeueCount;
14132
- const reviewWord = reviewCount === 1 ? "review" : "reviews";
14133
- const newCardWord = newCount === 1 ? "new card" : "new cards";
14134
- return `${reviewCount} ${reviewWord}, ${newCount} ${newCardWord}`;
14265
+ const supplyCount = this.supplyQ.dequeueCount;
14266
+ const supplyWord = supplyCount === 1 ? "card" : "cards";
14267
+ return `${supplyCount} supply ${supplyWord} drawn`;
14135
14268
  }
14136
14269
  get detailedReport() {
14137
- return this.newQ.toString + "\n" + this.reviewQ.toString + "\n" + this.failedQ.toString;
14270
+ return this.supplyQ.toString + "\n" + this.failedQ.toString;
14138
14271
  }
14139
14272
  // @ts-expect-error NodeJS.Timeout type not available in browser context
14140
14273
  _intervalHandle;
@@ -14145,11 +14278,9 @@ var SessionController = class _SessionController extends Loggable {
14145
14278
  * @param getViewComponent - Function to resolve view components
14146
14279
  * @param mixer - Optional source mixer strategy (defaults to QuotaRoundRobinMixer)
14147
14280
  * @param options - Optional session-level configuration
14148
- * @param options.defaultBatchLimit - Default pipeline batch size (default: 20).
14281
+ * @param options.defaultBatchLimit - Default supply working-set size (default: 20).
14149
14282
  * Smaller values for newer users cause more frequent replans, keeping plans
14150
14283
  * 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
14284
  */
14154
14285
  constructor(sources, time, dataLayer, getViewComponent, mixer, options) {
14155
14286
  super();
@@ -14172,17 +14303,13 @@ var SessionController = class _SessionController extends Loggable {
14172
14303
  if (options?.defaultBatchLimit !== void 0) {
14173
14304
  this._defaultBatchLimit = options.defaultBatchLimit;
14174
14305
  }
14175
- if (options?.initialReviewCap !== void 0) {
14176
- this._initialReviewCap = options.initialReviewCap;
14177
- }
14178
14306
  if (options?.outcomeObservers?.length) {
14179
14307
  this._outcomeObservers = [...options.outcomeObservers];
14180
14308
  }
14181
14309
  this.log(`Session constructed:
14182
14310
  startTime: ${this.startTime}
14183
14311
  endTime: ${this.endTime}
14184
- defaultBatchLimit: ${this._defaultBatchLimit}
14185
- initialReviewCap: ${this._initialReviewCap}`);
14312
+ defaultBatchLimit: ${this._defaultBatchLimit}`);
14186
14313
  registerActiveController(this);
14187
14314
  }
14188
14315
  tick() {
@@ -14216,15 +14343,6 @@ var SessionController = class _SessionController extends Loggable {
14216
14343
  this.log(`Failed card cleanup estimate: ${Math.round(ret)}`);
14217
14344
  return ret;
14218
14345
  }
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
14346
  async prepareSession() {
14229
14347
  if (this.sources.some((s) => typeof s.getWeightedCards !== "function")) {
14230
14348
  throw new Error(
@@ -14239,15 +14357,15 @@ var SessionController = class _SessionController extends Loggable {
14239
14357
  );
14240
14358
  }
14241
14359
  await this.hydrationService.ensureHydratedCards();
14242
- startSessionTracking(this.reviewQ.length, this.newQ.length, this.failedQ.length);
14360
+ startSessionTracking(this.supplyQ.length, this.failedQ.length);
14243
14361
  this._intervalHandle = setInterval(() => {
14244
14362
  this.tick();
14245
14363
  }, 1e3);
14246
14364
  }
14247
14365
  /**
14248
14366
  * Request a mid-session replan. Re-runs the pipeline with current user state
14249
- * and atomically replaces the newQ contents. Safe to call at any time during
14250
- * a session.
14367
+ * and atomically replaces (or merges into) the supplyQ contents. Safe to call
14368
+ * at any time during a session.
14251
14369
  *
14252
14370
  * Concurrency policy:
14253
14371
  * - Two unhinted auto-replans never run in parallel; the second coalesces
@@ -14261,7 +14379,8 @@ var SessionController = class _SessionController extends Loggable {
14261
14379
  * results (e.g. surfacing another gpc-intro card right after one
14262
14380
  * completed, skipping the prescribed `c-wst-*` follow-up).
14263
14381
  *
14264
- * Does NOT affect reviewQ or failedQ.
14382
+ * Re-pulls and re-ranks the whole supply (including reviews); does NOT affect
14383
+ * failedQ (controller-owned remediation).
14265
14384
  *
14266
14385
  * If nextCard() is called while a replan is in flight, it will automatically
14267
14386
  * await the replan before drawing from queues, ensuring the user always sees
@@ -14327,7 +14446,7 @@ var SessionController = class _SessionController extends Loggable {
14327
14446
  * excludeCards happen at *invocation* time, not at *queue* time. For a
14328
14447
  * queued replan that means excludes reflect the state after the prior
14329
14448
  * replan landed — which is what we want, since the prior replan's
14330
- * newQ.peek(0) is the imminent draw we need to exclude.
14449
+ * supplyQ.peek(0) is the imminent draw we need to exclude.
14331
14450
  */
14332
14451
  async _runReplan(opts) {
14333
14452
  this._activeReplanLabel = opts.label ?? "(auto)";
@@ -14340,8 +14459,8 @@ var SessionController = class _SessionController extends Loggable {
14340
14459
  for (const rec of this._sessionRecord) {
14341
14460
  excludeSet.add(rec.card.card_id);
14342
14461
  }
14343
- if (this.newQ.length > 0) {
14344
- excludeSet.add(this.newQ.peek(0).cardID);
14462
+ if (this.supplyQ.length > 0) {
14463
+ excludeSet.add(this.supplyQ.peek(0).cardID);
14345
14464
  }
14346
14465
  hints.excludeCards = [...excludeSet];
14347
14466
  if (opts.sessionHints !== void 0) {
@@ -14402,7 +14521,13 @@ var SessionController = class _SessionController extends Loggable {
14402
14521
  const describe = (q) => {
14403
14522
  const cards = [];
14404
14523
  for (let i = 0; i < q.length; i++) {
14405
- cards.push(q.peek(i).cardID);
14524
+ const item = q.peek(i);
14525
+ cards.push({
14526
+ cardID: item.cardID,
14527
+ status: item.status,
14528
+ origin: isReview(item) ? "review" : "new",
14529
+ score: item.score
14530
+ });
14406
14531
  }
14407
14532
  return { length: q.length, dequeueCount: q.dequeueCount, cards };
14408
14533
  };
@@ -14425,9 +14550,9 @@ var SessionController = class _SessionController extends Loggable {
14425
14550
  sessionHints: this._sessionHints,
14426
14551
  replanActive: this._replanPromise !== null,
14427
14552
  replanLabel: this._activeReplanLabel,
14428
- reviewQ: describe(this.reviewQ),
14429
- newQ: describe(this.newQ),
14553
+ supplyQ: describe(this.supplyQ),
14430
14554
  failedQ: describe(this.failedQ),
14555
+ reviewBacklog: getSrsBacklogDebug(),
14431
14556
  drawnCards
14432
14557
  };
14433
14558
  }
@@ -14547,7 +14672,7 @@ var SessionController = class _SessionController extends Loggable {
14547
14672
  */
14548
14673
  static WELL_INDICATED_SCORE = 0.1;
14549
14674
  /**
14550
- * newQ length at or below which the opportunistic depletion-prefetch
14675
+ * supplyQ length at or below which the opportunistic depletion-prefetch
14551
14676
  * fires. Sets the lead time available for the background replan to land
14552
14677
  * before the user actually empties the queue and falls into the
14553
14678
  * (synchronous) wedge-breaker path.
@@ -14560,7 +14685,7 @@ var SessionController = class _SessionController extends Loggable {
14560
14685
  */
14561
14686
  static DEPLETION_PREFETCH_THRESHOLD = 3;
14562
14687
  /**
14563
- * Internal replan execution. Runs the pipeline, builds a new newQ,
14688
+ * Internal replan execution. Runs the pipeline, rebuilds the supplyQ,
14564
14689
  * atomically swaps it in, and triggers hydration for the new contents.
14565
14690
  *
14566
14691
  * If the initial replan produces fewer than MIN_WELL_INDICATED cards that
@@ -14589,8 +14714,8 @@ var SessionController = class _SessionController extends Loggable {
14589
14714
  }
14590
14715
  await this.hydrationService.ensureHydratedCards();
14591
14716
  const labelTag = opts.label ? ` [${opts.label}]` : "";
14592
- this.log(`Replan complete${labelTag}: newQ now has ${this.newQ.length} cards (mode=${mode})`);
14593
- snapshotQueues(this.reviewQ.length, this.newQ.length, this.failedQ.length);
14717
+ this.log(`Replan complete${labelTag}: supplyQ now has ${this.supplyQ.length} cards (mode=${mode})`);
14718
+ snapshotQueues(this.supplyQ.length, this.failedQ.length);
14594
14719
  }
14595
14720
  addTime(seconds) {
14596
14721
  this.endTime = new Date(this.endTime.valueOf() + 1e3 * seconds);
@@ -14599,10 +14724,10 @@ var SessionController = class _SessionController extends Loggable {
14599
14724
  return this.failedQ.length;
14600
14725
  }
14601
14726
  toString() {
14602
- return `Session: ${this.reviewQ.length} Reviews, ${this.newQ.length} New, ${this.failedQ.length} failed`;
14727
+ return `Session: ${this.supplyQ.length} supply, ${this.failedQ.length} failed`;
14603
14728
  }
14604
14729
  reportString() {
14605
- return `${this.reviewQ.dequeueCount} Reviews, ${this.newQ.dequeueCount} New, ${this.failedQ.dequeueCount} failed`;
14730
+ return `${this.supplyQ.dequeueCount} supply, ${this.failedQ.dequeueCount} failed`;
14606
14731
  }
14607
14732
  /**
14608
14733
  * Returns debug information about the current session state.
@@ -14619,7 +14744,8 @@ var SessionController = class _SessionController extends Loggable {
14619
14744
  items.push({
14620
14745
  courseID: item.courseID || "unknown",
14621
14746
  cardID: item.cardID || "unknown",
14622
- status: item.status || "unknown"
14747
+ status: item.status || "unknown",
14748
+ score: item.score
14623
14749
  });
14624
14750
  }
14625
14751
  return items;
@@ -14629,15 +14755,10 @@ var SessionController = class _SessionController extends Loggable {
14629
14755
  mode: supportsWeightedCards ? "weighted" : "legacy",
14630
14756
  description: supportsWeightedCards ? "Using getWeightedCards() API with scored candidates" : "ERROR: getWeightedCards() not a function."
14631
14757
  },
14632
- reviewQueue: {
14633
- length: this.reviewQ.length,
14634
- dequeueCount: this.reviewQ.dequeueCount,
14635
- items: extractQueueItems(this.reviewQ)
14636
- },
14637
- newQueue: {
14638
- length: this.newQ.length,
14639
- dequeueCount: this.newQ.dequeueCount,
14640
- items: extractQueueItems(this.newQ)
14758
+ supplyQueue: {
14759
+ length: this.supplyQ.length,
14760
+ dequeueCount: this.supplyQ.dequeueCount,
14761
+ items: extractQueueItems(this.supplyQ)
14641
14762
  },
14642
14763
  failedQueue: {
14643
14764
  length: this.failedQ.length,
@@ -14657,30 +14778,29 @@ var SessionController = class _SessionController extends Loggable {
14657
14778
  };
14658
14779
  }
14659
14780
  /**
14660
- * Fetch content using the getWeightedCards API and mix across sources.
14781
+ * Fetch weighted content from all sources, mix across sources, and populate
14782
+ * the single supply queue in pipeline rank order.
14661
14783
  *
14662
- * This method:
14663
- * 1. Fetches weighted cards from each source
14664
- * 2. Fetches full review data (we need ScheduledCard fields for queue)
14665
- * 3. Uses SourceMixer to balance content across sources
14666
- * 4. Populates review and new card queues with mixed results
14667
- */
14668
- /**
14669
- * Fetch weighted content from all sources and populate session queues.
14784
+ * Reviews and new cards compete on one cross-comparable scale (SRS 0.5–1.0
14785
+ * w/ backlog pressure vs ELO 0.0–1.0) there is no origin split and no
14786
+ * second mixer. The working set is `supplyLimit` cards (the top of the mixed
14787
+ * ranking, plus any `+INF` required cards floated to the front); replans
14788
+ * re-pull and re-rank the whole supply, so a heavy review backlog surfaces as
14789
+ * a refreshed top-ranked working set rather than a frozen 200-card snapshot.
14670
14790
  *
14671
14791
  * @param options.replan - If true, this is a mid-session replan rather than
14672
- * initial session setup. Skips review queue population (avoiding duplicates),
14673
- * atomically replaces newQ contents, and treats empty results as non-fatal.
14674
- * @param options.additive - If true (replan only), merge new high-quality
14675
- * candidates into the front of the existing newQ instead of replacing it.
14792
+ * initial session setup. Atomically replaces supplyQ contents and treats
14793
+ * empty results as non-fatal.
14794
+ * @param options.additive - If true (replan only), merge high-quality
14795
+ * candidates into the front of the existing supplyQ instead of replacing it.
14676
14796
  * @returns Number of "well-indicated" cards (passed all hierarchy filters)
14677
14797
  * in the new content. Returns -1 if no content was loaded.
14678
14798
  */
14679
14799
  async getWeightedContent(options) {
14680
14800
  const replan = options?.replan ?? false;
14681
14801
  const additive = options?.additive ?? false;
14682
- const newLimit = options?.limit ?? this._defaultBatchLimit;
14683
- const fetchLimit = replan ? newLimit : newLimit + this._initialReviewCap;
14802
+ const supplyLimit = options?.limit ?? this._defaultBatchLimit;
14803
+ const fetchLimit = supplyLimit;
14684
14804
  if (!replan) {
14685
14805
  this._applyHintsToSources();
14686
14806
  }
@@ -14702,7 +14822,7 @@ var SessionController = class _SessionController extends Loggable {
14702
14822
  }
14703
14823
  if (batches.length === 0) {
14704
14824
  if (replan) {
14705
- this.log("Replan: no content from any source, keeping existing newQ");
14825
+ this.log("Replan: no content from any source, keeping existing supplyQ");
14706
14826
  return -1;
14707
14827
  }
14708
14828
  throw new Error(
@@ -14736,64 +14856,59 @@ var SessionController = class _SessionController extends Loggable {
14736
14856
  quotaPerSource,
14737
14857
  mixedWeighted
14738
14858
  );
14739
- const reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review").slice(0, this._initialReviewCap);
14740
- const newCandidates = mixedWeighted.filter(
14741
- (w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)
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 = [
14859
+ const candidates = mixedWeighted.filter((w) => !this._servedCardIds.has(w.cardId));
14860
+ const mandatoryWeighted = candidates.filter((w) => w.score === Number.POSITIVE_INFINITY);
14861
+ const optionalWeighted = candidates.filter((w) => w.score !== Number.POSITIVE_INFINITY);
14862
+ const supplyWeighted = [
14746
14863
  ...mandatoryWeighted,
14747
- ...optionalWeighted.slice(0, Math.max(0, newLimit - mandatoryWeighted.length))
14864
+ ...optionalWeighted.slice(0, Math.max(0, supplyLimit - mandatoryWeighted.length))
14748
14865
  ];
14749
14866
  const mandatoryIds = new Set(mandatoryWeighted.map((w) => w.cardId));
14750
- logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
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(
14867
+ const wellIndicated = supplyWeighted.filter(
14768
14868
  (w) => w.score >= _SessionController.WELL_INDICATED_SCORE
14769
14869
  ).length;
14770
- const newItems = [];
14771
- for (const w of newWeighted) {
14772
- const newItem = {
14773
- cardID: w.cardId,
14774
- courseID: w.courseId,
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)})
14870
+ let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
14871
+ const supplyItems = supplyWeighted.map((w) => {
14872
+ const origin = getCardOrigin(w);
14873
+ const scoreStr = Number.isFinite(w.score) ? w.score.toFixed(2) : "+INF";
14874
+ report += `${origin === "review" ? "Review" : "New"}: ${w.courseId}::${w.cardId} (score: ${scoreStr})
14781
14875
  `;
14782
- }
14876
+ return this._buildSupplyItem(w, origin);
14877
+ });
14783
14878
  if (additive) {
14784
- const added = this.newQ.mergeToFront(newItems, (item) => item.cardID, mandatoryIds);
14785
- report += `Additive merge: ${added} new cards added to front of newQ
14879
+ const added = this.supplyQ.mergeToFront(supplyItems, (item) => item.cardID, mandatoryIds);
14880
+ report += `Additive merge: ${added} cards added to front of supplyQ
14786
14881
  `;
14787
14882
  } else if (replan) {
14788
- this.newQ.replaceAll(newItems, (item) => item.cardID);
14883
+ this.supplyQ.replaceAll(supplyItems, (item) => item.cardID);
14789
14884
  } else {
14790
- for (const item of newItems) {
14791
- this.newQ.add(item, item.cardID);
14885
+ for (const item of supplyItems) {
14886
+ this.supplyQ.add(item, item.cardID);
14792
14887
  }
14793
14888
  }
14794
14889
  this.log(report);
14795
14890
  return wellIndicated;
14796
14891
  }
14892
+ /**
14893
+ * Build a supply item from a weighted candidate. Review-origin cards carry
14894
+ * their `reviewID` so SRS outcome tracking and re-presentation work; new
14895
+ * cards do not. `score` is carried on both for the debug overlay.
14896
+ */
14897
+ _buildSupplyItem(w, origin = getCardOrigin(w)) {
14898
+ const base = {
14899
+ cardID: w.cardId,
14900
+ courseID: w.courseId,
14901
+ contentSourceType: "course",
14902
+ contentSourceID: w.courseId,
14903
+ score: w.score
14904
+ };
14905
+ if (origin === "review") {
14906
+ const reviewItem = { ...base, status: "review", reviewID: w.reviewID };
14907
+ return reviewItem;
14908
+ }
14909
+ const newItem = { ...base, status: "new" };
14910
+ return newItem;
14911
+ }
14797
14912
  /**
14798
14913
  * Returns items that should be pre-hydrated.
14799
14914
  * Deterministic: top N items from each queue to ensure coverage.
@@ -14801,71 +14916,73 @@ var SessionController = class _SessionController extends Loggable {
14801
14916
  */
14802
14917
  _getItemsToHydrate() {
14803
14918
  const items = [];
14804
- const ITEMS_PER_QUEUE = 2;
14805
- for (let i = 0; i < Math.min(ITEMS_PER_QUEUE, this.reviewQ.length); i++) {
14806
- items.push(this.reviewQ.peek(i));
14807
- }
14808
- for (let i = 0; i < Math.min(ITEMS_PER_QUEUE, this.newQ.length); i++) {
14809
- items.push(this.newQ.peek(i));
14919
+ const SUPPLY_PREFETCH = 3;
14920
+ const FAILED_PREFETCH = 2;
14921
+ for (let i = 0; i < Math.min(SUPPLY_PREFETCH, this.supplyQ.length); i++) {
14922
+ items.push(this.supplyQ.peek(i));
14810
14923
  }
14811
- for (let i = 0; i < Math.min(ITEMS_PER_QUEUE, this.failedQ.length); i++) {
14924
+ for (let i = 0; i < Math.min(FAILED_PREFETCH, this.failedQ.length); i++) {
14812
14925
  items.push(this.failedQ.peek(i));
14813
14926
  }
14814
14927
  return items;
14815
14928
  }
14816
14929
  /**
14817
14930
  * Selects the next item to present to the user.
14818
- * Nondeterministic: uses probability to balance between queues based on session state.
14931
+ *
14932
+ * The supplyQ is already rank-ordered (the pipeline + mixer did the mixing,
14933
+ * with `+INF` required cards floated to the front), so the primary path is a
14934
+ * deterministic front-to-back draw — no second new-vs-review mixer. The only
14935
+ * remaining decisions are (a) when the session ends and (b) when to interleave
14936
+ * a remediation card from failedQ. See decision doc §2/§3/§7.
14819
14937
  */
14820
14938
  _selectNextItemToHydrate() {
14821
- const choice = Math.random();
14822
- let newBound = 0.1;
14823
- let reviewBound = 0.75;
14824
- if (this.reviewQ.length === 0 && this.failedQ.length === 0 && this.newQ.length === 0) {
14939
+ if (this.supplyQ.length === 0 && this.failedQ.length === 0) {
14825
14940
  return null;
14826
14941
  }
14827
14942
  if (this._secondsRemaining < 2 && this.failedQ.length === 0 && this._minCardsGuarantee <= 0) {
14828
14943
  return null;
14829
14944
  }
14830
14945
  if (this._secondsRemaining <= 0 && this._minCardsGuarantee <= 0) {
14831
- if (this.failedQ.length > 0) {
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);
14946
+ return this.failedQ.length > 0 ? this.failedQ.peek(0) : null;
14839
14947
  }
14840
- const cleanupTime = this.estimateCleanupTime();
14841
- const reviewTime = this.estimateReviewTime();
14842
- const availableTime = this._secondsRemaining - (cleanupTime + reviewTime);
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;
14948
+ const supplyTop = this.supplyQ.length > 0 ? this.supplyQ.peek(0) : null;
14949
+ if (this._minCardsGuarantee > 0 && supplyTop) {
14950
+ return supplyTop;
14852
14951
  }
14853
- if (this.failedQ.length === 0) {
14854
- reviewBound = 1;
14952
+ if (this.failedQ.length > 0 && this._shouldInterleaveFailed(supplyTop !== null)) {
14953
+ return this.failedQ.peek(0);
14855
14954
  }
14856
- if (this.reviewQ.length === 0) {
14857
- newBound = reviewBound;
14955
+ if (supplyTop) {
14956
+ return supplyTop;
14858
14957
  }
14859
- if (choice < newBound && this.newQ.length) {
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) {
14958
+ if (this.failedQ.length > 0) {
14864
14959
  return this.failedQ.peek(0);
14865
- } else {
14866
- this.log(`No more cards available for the session!`);
14867
- return null;
14868
14960
  }
14961
+ this.log(`No more cards available for the session!`);
14962
+ return null;
14963
+ }
14964
+ /** Supply draws between forced failed-queue interleaves (light steady cadence). */
14965
+ static FAILED_INTERLEAVE_EVERY = 4;
14966
+ /**
14967
+ * Slack (seconds) below which the endgame failed-pressure kicks in: when the
14968
+ * time left after clearing remediation drops under this, bias hard to failed
14969
+ * so the session doesn't end with un-cleared remediation. Mirrors the old
14970
+ * `availableTime > 20` ladder thresholds.
14971
+ */
14972
+ static FAILED_ENDGAME_SLACK_SECONDS = 20;
14973
+ /**
14974
+ * Whether to interleave a failed (remediation) card now instead of drawing
14975
+ * the supply head. Replaces the old `newBound`/`reviewBound` probability
14976
+ * ladder's failed path (decision doc §7).
14977
+ *
14978
+ * @param supplyAvailable - whether supplyQ has a card to draw instead.
14979
+ */
14980
+ _shouldInterleaveFailed(supplyAvailable) {
14981
+ if (this.failedQ.length === 0) return false;
14982
+ if (!supplyAvailable) return true;
14983
+ const availableTime = this._secondsRemaining - this.estimateCleanupTime();
14984
+ if (availableTime <= _SessionController.FAILED_ENDGAME_SLACK_SECONDS) return true;
14985
+ return this._supplyDrawsSinceFailed >= _SessionController.FAILED_INTERLEAVE_EVERY;
14869
14986
  }
14870
14987
  async nextCard(action = "dismiss-success") {
14871
14988
  this.dismissCurrentCard(action);
@@ -14873,22 +14990,21 @@ var SessionController = class _SessionController extends Loggable {
14873
14990
  this._minCardsGuarantee--;
14874
14991
  this.log(`[CardGuarantee] ${this._minCardsGuarantee} guaranteed cards remaining`);
14875
14992
  }
14876
- if (this._replanPromise && this.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
14993
+ if (this._replanPromise && this.supplyQ.length === 0 && this.failedQ.length === 0) {
14877
14994
  this.log("nextCard: queues empty, awaiting in-flight replan before drawing");
14878
14995
  await this._replanPromise;
14879
14996
  }
14880
- if (this.newQ.length <= _SessionController.DEPLETION_PREFETCH_THRESHOLD && this._secondsRemaining > 0 && !this._replanPromise) {
14997
+ if (this.supplyQ.length <= _SessionController.DEPLETION_PREFETCH_THRESHOLD && this._secondsRemaining > 0 && !this._replanPromise) {
14881
14998
  this._suppressQualityReplan = false;
14882
- const otherContent = this.reviewQ.length + this.failedQ.length;
14883
14999
  this.log(
14884
- `[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
15000
+ `[AutoReplan:depletion] supplyQ has ${this.supplyQ.length} card(s) (${this.failedQ.length} failed pending) with ${this._secondsRemaining}s remaining. Triggering background replan.`
14885
15001
  );
14886
15002
  void this.requestReplan({ label: "auto:depletion", mode: "merge" });
14887
15003
  }
14888
15004
  const REPLAN_BUFFER = 3;
14889
- if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
15005
+ if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.supplyQ.length > 0 && !this._replanPromise) {
14890
15006
  this.log(
14891
- `[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (newQ: ${this.newQ.length}). Triggering background replan.`
15007
+ `[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (supplyQ: ${this.supplyQ.length}). Triggering background replan.`
14892
15008
  );
14893
15009
  void this.requestReplan({ label: "auto:quality" });
14894
15010
  }
@@ -14900,12 +15016,12 @@ var SessionController = class _SessionController extends Loggable {
14900
15016
  const WEDGE_MAX_EMPTY_STREAK = 3;
14901
15017
  const WEDGE_BACKOFF_MS = 250;
14902
15018
  let wedgeEmptyStreak = 0;
14903
- while (this._secondsRemaining > 0 && this.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
15019
+ while (this._secondsRemaining > 0 && this.supplyQ.length === 0 && this.failedQ.length === 0) {
14904
15020
  this.log(
14905
15021
  `[WedgeBreaker] All queues empty with ${this._secondsRemaining}s remaining. Running pipeline (attempt ${wedgeEmptyStreak + 1}/${WEDGE_MAX_EMPTY_STREAK}).`
14906
15022
  );
14907
15023
  await this._replanUncoalesced({ label: "wedge-breaker" });
14908
- if (this.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
15024
+ if (this.supplyQ.length === 0 && this.failedQ.length === 0) {
14909
15025
  wedgeEmptyStreak++;
14910
15026
  if (wedgeEmptyStreak >= WEDGE_MAX_EMPTY_STREAK) {
14911
15027
  this.log(
@@ -14935,15 +15051,16 @@ var SessionController = class _SessionController extends Loggable {
14935
15051
  await this.hydrationService.ensureHydratedCards();
14936
15052
  this._currentCard = card;
14937
15053
  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" : nextItem.status === "review" ? "reviewQ" : "newQ";
15054
+ const queueSource = nextItem.status.startsWith("failed") ? "failedQ" : "supplyQ";
14939
15055
  recordCardPresentation(
14940
15056
  nextItem.cardID,
14941
15057
  nextItem.courseID,
14942
15058
  this.courseNameCache.get(nextItem.courseID),
14943
15059
  origin,
14944
- queueSource
15060
+ queueSource,
15061
+ nextItem.score
14945
15062
  );
14946
- snapshotQueues(this.reviewQ.length, this.newQ.length, this.failedQ.length);
15063
+ snapshotQueues(this.supplyQ.length, this.failedQ.length);
14947
15064
  return card;
14948
15065
  }
14949
15066
  this.log(`Skipping card ${nextItem.cardID}: hydration failed, trying next`);
@@ -15013,6 +15130,7 @@ var SessionController = class _SessionController extends Loggable {
15013
15130
  };
15014
15131
  }
15015
15132
  this.failedQ.add(failedItem, failedItem.cardID);
15133
+ this._supplyDrawsSinceFailed = 0;
15016
15134
  } else if (action === "dismiss-error") {
15017
15135
  this.hydrationService.removeCard(this._currentCard.item.cardID);
15018
15136
  } else if (action === "dismiss-failed") {
@@ -15026,15 +15144,15 @@ var SessionController = class _SessionController extends Loggable {
15026
15144
  removeItemFromQueue(item) {
15027
15145
  this._clearDurableRequirement(item.cardID);
15028
15146
  this._servedCardIds.add(item.cardID);
15029
- if (this.reviewQ.peek(0)?.cardID === item.cardID) {
15030
- this.reviewQ.dequeue((queueItem) => queueItem.cardID);
15031
- } else if (this.newQ.peek(0)?.cardID === item.cardID) {
15032
- this.newQ.dequeue((queueItem) => queueItem.cardID);
15147
+ if (this.failedQ.peek(0)?.cardID === item.cardID) {
15148
+ this.failedQ.dequeue((queueItem) => queueItem.cardID);
15149
+ this._supplyDrawsSinceFailed = 0;
15150
+ } else if (this.supplyQ.peek(0)?.cardID === item.cardID) {
15151
+ this.supplyQ.dequeue((queueItem) => queueItem.cardID);
15152
+ this._supplyDrawsSinceFailed++;
15033
15153
  if (this._wellIndicatedRemaining > 0) {
15034
15154
  this._wellIndicatedRemaining--;
15035
15155
  }
15036
- } else if (this.failedQ.peek(0)?.cardID === item.cardID) {
15037
- this.failedQ.dequeue((queueItem) => queueItem.cardID);
15038
15156
  }
15039
15157
  }
15040
15158
  /**
@@ -15134,6 +15252,7 @@ init_factory();
15134
15252
  areQuestionRecords,
15135
15253
  buildStrategyStateId,
15136
15254
  captureMixerRun,
15255
+ clearSrsBacklogDebug,
15137
15256
  computeDeviation,
15138
15257
  computeEffectiveWeight,
15139
15258
  computeOutcomeSignal,
@@ -15154,6 +15273,7 @@ init_factory();
15154
15273
  getRegisteredNavigator,
15155
15274
  getRegisteredNavigatorNames,
15156
15275
  getRegisteredNavigatorRole,
15276
+ getSrsBacklogDebug,
15157
15277
  getStudySource,
15158
15278
  hasRegisteredNavigator,
15159
15279
  importParsedCards,