@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.
Files changed (39) 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 +135 -41
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +133 -41
  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 +131 -41
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +131 -41
  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 +374 -253
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +372 -253
  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/impl/couch/courseDB.ts +11 -2
  37. package/src/study/SessionController.ts +260 -249
  38. package/src/study/SessionDebugger.ts +15 -25
  39. 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 lets the pipeline's
2611
- * scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
2612
- * this method's job; it only guarantees presence.
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: BASE_PRACTICE_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: BASE_PRACTICE_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, MAX_BACKLOG_PRESSURE, SRSNavigator;
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
- MAX_BACKLOG_PRESSURE = 0.5;
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 backlogPressure = this.computeBacklogPressure(dueReviews.length);
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 = backlogPressure > 0 ? ` [backlog pressure: +${backlogPressure.toFixed(2)}]` : ` [healthy backlog]`;
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, backlogPressure);
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
- return { cards: scored.sort((a, b) => b.score - a.score).slice(0, limit) };
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
- * Backlog pressure is 0 when at or below healthy threshold,
2956
- * and increases linearly above it, maxing out at MAX_BACKLOG_PRESSURE.
3039
+ * ×1.0 at or below the healthy threshold (no boost), increasing linearly above
3040
+ * it and maxing out at MAX_BACKLOG_MULTIPLIER at the healthy backlog.
2957
3041
  *
2958
- * Examples (with default healthyBacklog=20):
2959
- * - 10 due reviews → 0.00 (healthy)
2960
- * - 20 due reviews → 0.00 (at threshold)
2961
- * - 40 due reviews → 0.25 (2x threshold)
2962
- * - 60 due reviews → 0.50 (3x threshold, maxed)
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 Backlog pressure score to add to urgency (0 to MAX_BACKLOG_PRESSURE)
3049
+ * @returns Multiplier applied to review urgency (1.0 to MAX_BACKLOG_MULTIPLIER)
2966
3050
  */
2967
- computeBacklogPressure(dueCount) {
3051
+ computeBacklogMultiplier(dueCount) {
2968
3052
  if (dueCount <= this.healthyBacklog) {
2969
- return 0;
3053
+ return 1;
2970
3054
  }
2971
3055
  const excess = dueCount - this.healthyBacklog;
2972
- const pressure = excess / this.healthyBacklog * (MAX_BACKLOG_PRESSURE / 2);
2973
- return Math.min(MAX_BACKLOG_PRESSURE, pressure);
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 boost when review backlog exceeds healthy threshold
2989
- * - At healthy backlog: 0
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 + (urgency factors * 0.45) + backlog pressure
2994
- * Result range: 0.5 to 1.0 (uncapped to allow high-urgency reviews to compete with new cards)
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 backlogPressure - Pre-computed backlog pressure (0 to 0.5)
3083
+ * @param backlogMultiplier - Pre-computed backlog multiplier (1.0 to MAX_BACKLOG_MULTIPLIER)
2999
3084
  */
3000
- computeUrgencyScore(review, now, backlogPressure) {
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 = Math.min(1, baseScore + backlogPressure);
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 (backlogPressure > 0) {
3018
- reasonParts.push(`backlog: +${backlogPressure.toFixed(2)}`);
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: "CARD-",
6044
- endkey: "CARD-\uFFF0",
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
- reviewQ: false,
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) + queueHtml("reviewQ", "reviewQ", s.reviewQ) + queueHtml("newQ", "newQ", s.newQ) + queueHtml("failedQ", "failedQ", s.failedQ) + drawnHtml("drawn", s.drawnCards);
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">${esc(c)}</li>`).join("") + `</ol>`;
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) => lines.push(` ${i + 1}. ${c}`));
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("reviewQ", s.reviewQ);
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(reviewQLength, newQLength, failedQLength) {
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
- reviewQLength,
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(reviewQLength, newQLength, failedQLength, reviewQNext3, newQNext3) {
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
- reviewQLength,
13672
- newQLength,
13800
+ supplyQLength,
13673
13801
  failedQLength,
13674
- reviewQNext3,
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(`Review Queue: ${latest.reviewQLength} cards`);
13698
- if (latest.reviewQNext3 && latest.reviewQNext3.length > 0) {
13699
- logger.info(` Next: ${latest.reviewQNext3.join(", ")}`);
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
- reviewQ = new ItemQueue();
13938
- newQ = new ItemQueue();
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 new cards remaining before the queue
13955
- * degrades to poorly-indicated content. Decremented on each newQ
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 (newQ exhausted).
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 newQ on every (re)plan: a `new`
13994
- * card shown once must never re-enter newQ this session. This is the general
13995
- * guard against re-presentation including the case where a replan in flight
13996
- * captured a now-drawn card (e.g. a +INF require-injected follow-up the
13997
- * depletion prefetch grabbed just before it was drawn). Reviews/failed cards
13998
- * legitimately recur and are tracked by their own queues, so this only gates
13999
- * `new`-origin candidates.
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 reviewCount = this.reviewQ.dequeueCount;
14025
- const newCount = this.newQ.dequeueCount;
14026
- const reviewWord = reviewCount === 1 ? "review" : "reviews";
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.newQ.toString + "\n" + this.reviewQ.toString + "\n" + this.failedQ.toString;
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 pipeline batch size (default: 20).
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.reviewQ.length, this.newQ.length, this.failedQ.length);
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 newQ contents. Safe to call at any time during
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
- * Does NOT affect reviewQ or failedQ.
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
- * newQ.peek(0) is the imminent draw we need to exclude.
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.newQ.length > 0) {
14238
- excludeSet.add(this.newQ.peek(0).cardID);
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
- cards.push(q.peek(i).cardID);
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
- reviewQ: describe(this.reviewQ),
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
- * newQ length at or below which the opportunistic depletion-prefetch
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, builds a new newQ,
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}: newQ now has ${this.newQ.length} cards (mode=${mode})`);
14487
- snapshotQueues(this.reviewQ.length, this.newQ.length, this.failedQ.length);
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.reviewQ.length} Reviews, ${this.newQ.length} New, ${this.failedQ.length} failed`;
14620
+ return `Session: ${this.supplyQ.length} supply, ${this.failedQ.length} failed`;
14497
14621
  }
14498
14622
  reportString() {
14499
- return `${this.reviewQ.dequeueCount} Reviews, ${this.newQ.dequeueCount} New, ${this.failedQ.dequeueCount} failed`;
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
- reviewQueue: {
14527
- length: this.reviewQ.length,
14528
- dequeueCount: this.reviewQ.dequeueCount,
14529
- items: extractQueueItems(this.reviewQ)
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 using the getWeightedCards API and mix across sources.
14674
+ * Fetch weighted content from all sources, mix across sources, and populate
14675
+ * the single supply queue in pipeline rank order.
14555
14676
  *
14556
- * This method:
14557
- * 1. Fetches weighted cards from each source
14558
- * 2. Fetches full review data (we need ScheduledCard fields for queue)
14559
- * 3. Uses SourceMixer to balance content across sources
14560
- * 4. Populates review and new card queues with mixed results
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. Skips review queue population (avoiding duplicates),
14567
- * atomically replaces newQ contents, and treats empty results as non-fatal.
14568
- * @param options.additive - If true (replan only), merge new high-quality
14569
- * candidates into the front of the existing newQ instead of replacing it.
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 newLimit = options?.limit ?? this._defaultBatchLimit;
14577
- const fetchLimit = replan ? newLimit : newLimit + this._initialReviewCap;
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 newQ");
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 reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review").slice(0, this._initialReviewCap);
14634
- const newCandidates = mixedWeighted.filter(
14635
- (w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)
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, newLimit - mandatoryWeighted.length))
14757
+ ...optionalWeighted.slice(0, Math.max(0, supplyLimit - mandatoryWeighted.length))
14642
14758
  ];
14643
14759
  const mandatoryIds = new Set(mandatoryWeighted.map((w) => w.cardId));
14644
- logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
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
- const newItems = [];
14665
- for (const w of newWeighted) {
14666
- const newItem = {
14667
- cardID: w.cardId,
14668
- courseID: w.courseId,
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.newQ.mergeToFront(newItems, (item) => item.cardID, mandatoryIds);
14679
- report += `Additive merge: ${added} new cards added to front of newQ
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.newQ.replaceAll(newItems, (item) => item.cardID);
14776
+ this.supplyQ.replaceAll(supplyItems, (item) => item.cardID);
14683
14777
  } else {
14684
- for (const item of newItems) {
14685
- this.newQ.add(item, item.cardID);
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 ITEMS_PER_QUEUE = 2;
14699
- for (let i = 0; i < Math.min(ITEMS_PER_QUEUE, this.reviewQ.length); i++) {
14700
- items.push(this.reviewQ.peek(i));
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(ITEMS_PER_QUEUE, this.failedQ.length); i++) {
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
- * Nondeterministic: uses probability to balance between queues based on session state.
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
- const choice = Math.random();
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
- if (this.failedQ.length > 0) {
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 cleanupTime = this.estimateCleanupTime();
14735
- const reviewTime = this.estimateReviewTime();
14736
- const availableTime = this._secondsRemaining - (cleanupTime + reviewTime);
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 === 0) {
14748
- reviewBound = 1;
14845
+ if (this.failedQ.length > 0 && this._shouldInterleaveFailed(supplyTop !== null)) {
14846
+ return this.failedQ.peek(0);
14749
14847
  }
14750
- if (this.reviewQ.length === 0) {
14751
- newBound = reviewBound;
14848
+ if (supplyTop) {
14849
+ return supplyTop;
14752
14850
  }
14753
- if (choice < newBound && this.newQ.length) {
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.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
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.newQ.length <= _SessionController.DEPLETION_PREFETCH_THRESHOLD && this._secondsRemaining > 0 && !this._replanPromise) {
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] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
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.newQ.length > 0 && !this._replanPromise) {
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 (newQ: ${this.newQ.length}). Triggering background replan.`
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.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
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.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
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" : nextItem.status === "review" ? "reviewQ" : "newQ";
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.reviewQ.length, this.newQ.length, this.failedQ.length);
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.reviewQ.peek(0)?.cardID === item.cardID) {
14924
- this.reviewQ.dequeue((queueItem) => queueItem.cardID);
14925
- } else if (this.newQ.peek(0)?.cardID === item.cardID) {
14926
- this.newQ.dequeue((queueItem) => queueItem.cardID);
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,