@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.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();
@@ -13338,6 +13427,7 @@ mountMixerDebugger();
13338
13427
  // src/study/SessionDebugger.ts
13339
13428
  init_logger();
13340
13429
  init_PipelineDebugger();
13430
+ init_SrsDebugger();
13341
13431
 
13342
13432
  // src/study/SessionOverlay.ts
13343
13433
  init_logger();
@@ -13359,8 +13449,7 @@ var lastSnapshot = null;
13359
13449
  var copyFlashUntil = 0;
13360
13450
  var minified = false;
13361
13451
  var expanded = {
13362
- reviewQ: false,
13363
- newQ: false,
13452
+ supplyQ: false,
13364
13453
  failedQ: false,
13365
13454
  drawn: false
13366
13455
  };
@@ -13429,7 +13518,7 @@ function render() {
13429
13518
  attachHandlers();
13430
13519
  return;
13431
13520
  }
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);
13521
+ 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
13522
  attachHandlers();
13434
13523
  }
13435
13524
  function attachHandlers() {
@@ -13521,6 +13610,29 @@ function hintsHtml(h) {
13521
13610
  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
13611
  return `<div style="margin-bottom:6px"><div style="color:#86efac">sessionHints</div>${body}</div>`;
13523
13612
  }
13613
+ function backlogHtml(backlog) {
13614
+ if (!backlog.length) return "";
13615
+ const rows = backlog.map((b) => {
13616
+ const maxed = b.backlogMultiplier >= b.maxBacklogMultiplier - 1e-9;
13617
+ const multColor = b.backlogMultiplier <= 1 ? "#86efac" : maxed ? "#fca5a5" : "#fcd34d";
13618
+ const headroom = maxed ? "maxed \u2014 boosts decide order until they relax" : b.backlogMultiplier > 1 ? "climbing as backlog grows" : "healthy \u2014 no pressure";
13619
+ const top = b.topReviewScore !== null ? b.topReviewScore.toFixed(2) : "\u2014";
13620
+ const next = b.nextDueIn ? ` <span style="opacity:.6">\xB7 next due ${esc(b.nextDueIn)}</span>` : "";
13621
+ 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>`;
13622
+ }).join("");
13623
+ return `<div style="margin-bottom:6px"><div style="color:#93c5fd">review backpressure</div>${rows}</div>`;
13624
+ }
13625
+ function fmtScore(score) {
13626
+ if (score === void 0) return "";
13627
+ if (!Number.isFinite(score)) return "REQ";
13628
+ return score.toFixed(2);
13629
+ }
13630
+ function queueItemHtml(item) {
13631
+ const tagColor = item.origin === "review" ? "#93c5fd" : "#fcd34d";
13632
+ const score = fmtScore(item.score);
13633
+ const label = `${item.origin === "review" ? "r" : "n"}${score ? " " + score : ""}`;
13634
+ return `${esc(item.cardID)}<span style="color:${tagColor};opacity:.85"> [${label}]</span>`;
13635
+ }
13524
13636
  function queueHtml(key, label, q) {
13525
13637
  const collapsible = q.length > INLINE_THRESHOLD;
13526
13638
  const isOpen = collapsible && expanded[key];
@@ -13535,7 +13647,7 @@ function queueHtml(key, label, q) {
13535
13647
  const shown = isOpen ? q.cards : q.cards.slice(0, INLINE_THRESHOLD);
13536
13648
  const hiddenCount = q.length - shown.length;
13537
13649
  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>`;
13650
+ 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
13651
  if (collapsible) {
13540
13652
  const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
13541
13653
  body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
@@ -13598,13 +13710,29 @@ function snapshotToText(s) {
13598
13710
  if (h.excludeCards?.length) hintParts.push(` excludeCards: ${h.excludeCards.join(", ")}`);
13599
13711
  }
13600
13712
  lines.push(hintParts.length ? hintParts.join("\n") : " none");
13713
+ if (s.reviewBacklog.length) {
13714
+ lines.push("");
13715
+ lines.push("review backpressure:");
13716
+ for (const b of s.reviewBacklog) {
13717
+ const maxed = b.backlogMultiplier >= b.maxBacklogMultiplier - 1e-9;
13718
+ const headroom = maxed ? "maxed (boosts decide order)" : b.backlogMultiplier > 1 ? "climbing" : "healthy";
13719
+ const top = b.topReviewScore !== null ? b.topReviewScore.toFixed(2) : "\u2014";
13720
+ const next = b.nextDueIn ? `, next due ${b.nextDueIn}` : "";
13721
+ lines.push(
13722
+ ` ${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}`
13723
+ );
13724
+ }
13725
+ }
13601
13726
  const queueText = (label, q) => {
13602
13727
  lines.push("");
13603
13728
  lines.push(`${label}: ${q.length} (drawn ${q.dequeueCount})`);
13604
- q.cards.forEach((c, i) => lines.push(` ${i + 1}. ${c}`));
13729
+ q.cards.forEach((c, i) => {
13730
+ const score = fmtScore(c.score);
13731
+ const tag = `${c.origin === "review" ? "r" : "n"}${score ? " " + score : ""}`;
13732
+ lines.push(` ${i + 1}. ${c.cardID} [${tag}]`);
13733
+ });
13605
13734
  };
13606
- queueText("reviewQ", s.reviewQ);
13607
- queueText("newQ", s.newQ);
13735
+ queueText("supplyQ", s.supplyQ);
13608
13736
  queueText("failedQ", s.failedQ);
13609
13737
  lines.push("");
13610
13738
  lines.push(`drawn: ${s.drawnCards.length}`);
@@ -13629,16 +13757,16 @@ function esc(value) {
13629
13757
  var activeSession = null;
13630
13758
  var sessionHistory = [];
13631
13759
  var MAX_HISTORY = 5;
13632
- function startSessionTracking(reviewQLength, newQLength, failedQLength) {
13760
+ function startSessionTracking(supplyQLength, failedQLength) {
13633
13761
  clearRunHistory();
13762
+ clearSrsBacklogDebug();
13634
13763
  const sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
13635
13764
  activeSession = {
13636
13765
  sessionId,
13637
13766
  startTime: /* @__PURE__ */ new Date(),
13638
13767
  initialQueues: {
13639
13768
  timestamp: /* @__PURE__ */ new Date(),
13640
- reviewQLength,
13641
- newQLength,
13769
+ supplyQLength,
13642
13770
  failedQLength
13643
13771
  },
13644
13772
  presentations: [],
@@ -13662,17 +13790,15 @@ function recordCardPresentation(cardId, courseId, courseName, origin, queueSourc
13662
13790
  score
13663
13791
  });
13664
13792
  }
13665
- function snapshotQueues(reviewQLength, newQLength, failedQLength, reviewQNext3, newQNext3) {
13793
+ function snapshotQueues(supplyQLength, failedQLength, supplyQNext3) {
13666
13794
  if (!activeSession) {
13667
13795
  return;
13668
13796
  }
13669
13797
  activeSession.queueSnapshots.push({
13670
13798
  timestamp: /* @__PURE__ */ new Date(),
13671
- reviewQLength,
13672
- newQLength,
13799
+ supplyQLength,
13673
13800
  failedQLength,
13674
- reviewQNext3,
13675
- newQNext3
13801
+ supplyQNext3
13676
13802
  });
13677
13803
  }
13678
13804
  function endSessionTracking() {
@@ -13694,13 +13820,9 @@ function showCurrentQueue() {
13694
13820
  }
13695
13821
  const latest = activeSession.queueSnapshots[activeSession.queueSnapshots.length - 1] || activeSession.initialQueues;
13696
13822
  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(", ")}`);
13823
+ logger.info(`Supply Queue: ${latest.supplyQLength} cards`);
13824
+ if (latest.supplyQNext3 && latest.supplyQNext3.length > 0) {
13825
+ logger.info(` Next: ${latest.supplyQNext3.join(", ")}`);
13704
13826
  }
13705
13827
  logger.info(`Failed Queue: ${latest.failedQLength} cards`);
13706
13828
  console.groupEnd();
@@ -13917,15 +14039,6 @@ var SessionController = class _SessionController extends Loggable {
13917
14039
  * Individual replans can override via `ReplanOptions.limit`.
13918
14040
  */
13919
14041
  _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
14042
  sources;
13930
14043
  // dataLayer and getViewComponent now injected into CardHydrationService
13931
14044
  _sessionRecord = [];
@@ -13934,10 +14047,28 @@ var SessionController = class _SessionController extends Loggable {
13934
14047
  }
13935
14048
  // Session card stores
13936
14049
  _currentCard = null;
13937
- reviewQ = new ItemQueue();
13938
- newQ = new ItemQueue();
14050
+ /**
14051
+ * The single supply queue: `new` + `review` items interleaved in pipeline
14052
+ * rank order (the mixer's score-ordered, source-interleaved output, with
14053
+ * `+INF` required cards floated to the front). Drawn front-to-back; reviews
14054
+ * and new compete on one cross-comparable scale rather than being re-mixed
14055
+ * by a probability gate. Replaced/re-ranked wholesale on replan. See
14056
+ * `docs/decision-single-supply-queue.md`.
14057
+ */
14058
+ supplyQ = new ItemQueue();
13939
14059
  failedQ = new ItemQueue();
13940
14060
  // END Session card stores
14061
+ /**
14062
+ * Supply draws since the last failed-queue *event* (a failed draw, or a card
14063
+ * entering failedQ on failure). Drives the light steady failed-interleave
14064
+ * (§7): after this many consecutive supply draws, a pending failed card is
14065
+ * drawn so remediation doesn't starve mid-session. Incremented on each supply
14066
+ * draw; reset to 0 both when a failed card is drawn AND when one is added to
14067
+ * failedQ — the latter gives a just-failed card spacing instead of an instant
14068
+ * retry (the counter would otherwise already be ≥ threshold from the preceding
14069
+ * supply run).
14070
+ */
14071
+ _supplyDrawsSinceFailed = 0;
13941
14072
  /**
13942
14073
  * Promise tracking a currently in-progress replan, or null if idle.
13943
14074
  * Used by nextCard() to await completion before drawing from queues.
@@ -13951,8 +14082,8 @@ var SessionController = class _SessionController extends Loggable {
13951
14082
  */
13952
14083
  _activeReplanLabel = null;
13953
14084
  /**
13954
- * Number of well-indicated new cards remaining before the queue
13955
- * degrades to poorly-indicated content. Decremented on each newQ
14085
+ * Number of well-indicated supply cards remaining before the queue
14086
+ * degrades to poorly-indicated content. Decremented on each supplyQ
13956
14087
  * draw; when it hits 0, a replan is triggered automatically
13957
14088
  * (user state has changed from completing good cards).
13958
14089
  */
@@ -13961,7 +14092,7 @@ var SessionController = class _SessionController extends Loggable {
13961
14092
  * When true, suppresses the quality-based auto-replan trigger in
13962
14093
  * nextCard(). Set after a burst replan (small limit) to prevent the
13963
14094
  * auto-replan from clobbering the burst cards before they're consumed.
13964
- * Cleared when the depletion-triggered replan fires (newQ exhausted).
14095
+ * Cleared when the depletion-triggered replan fires (supplyQ exhausted).
13965
14096
  */
13966
14097
  _suppressQualityReplan = false;
13967
14098
  /**
@@ -13990,13 +14121,15 @@ var SessionController = class _SessionController extends Loggable {
13990
14121
  * a draw the instant it happens — earlier than `_sessionRecord`, which only
13991
14122
  * lands once the card is *responded to*.
13992
14123
  *
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.
14124
+ * Used to keep already-served cards out of supplyQ on every (re)plan, across
14125
+ * ALL origins: a `new` card shown once must never re-enter, and once replans
14126
+ * re-pull reviews, an answered/in-flight review must not re-enter the supply
14127
+ * before its SRS reschedule clears the due-window (the review-loop guard,
14128
+ * decision doc §4). This is the general guard against re-presentation —
14129
+ * including the case where a replan in flight captured a now-drawn card (e.g.
14130
+ * a +INF require-injected follow-up the depletion prefetch grabbed just before
14131
+ * it was drawn). failedQ is separate and controller-owned, so failed cards
14132
+ * legitimately recur there without being gated here.
14000
14133
  */
14001
14134
  _servedCardIds = /* @__PURE__ */ new Set();
14002
14135
  /**
@@ -14021,14 +14154,12 @@ var SessionController = class _SessionController extends Loggable {
14021
14154
  return this._minCardsGuarantee > 0;
14022
14155
  }
14023
14156
  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}`;
14157
+ const supplyCount = this.supplyQ.dequeueCount;
14158
+ const supplyWord = supplyCount === 1 ? "card" : "cards";
14159
+ return `${supplyCount} supply ${supplyWord} drawn`;
14029
14160
  }
14030
14161
  get detailedReport() {
14031
- return this.newQ.toString + "\n" + this.reviewQ.toString + "\n" + this.failedQ.toString;
14162
+ return this.supplyQ.toString + "\n" + this.failedQ.toString;
14032
14163
  }
14033
14164
  // @ts-expect-error NodeJS.Timeout type not available in browser context
14034
14165
  _intervalHandle;
@@ -14039,11 +14170,9 @@ var SessionController = class _SessionController extends Loggable {
14039
14170
  * @param getViewComponent - Function to resolve view components
14040
14171
  * @param mixer - Optional source mixer strategy (defaults to QuotaRoundRobinMixer)
14041
14172
  * @param options - Optional session-level configuration
14042
- * @param options.defaultBatchLimit - Default pipeline batch size (default: 20).
14173
+ * @param options.defaultBatchLimit - Default supply working-set size (default: 20).
14043
14174
  * Smaller values for newer users cause more frequent replans, keeping plans
14044
14175
  * 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
14176
  */
14048
14177
  constructor(sources, time, dataLayer, getViewComponent, mixer, options) {
14049
14178
  super();
@@ -14066,17 +14195,13 @@ var SessionController = class _SessionController extends Loggable {
14066
14195
  if (options?.defaultBatchLimit !== void 0) {
14067
14196
  this._defaultBatchLimit = options.defaultBatchLimit;
14068
14197
  }
14069
- if (options?.initialReviewCap !== void 0) {
14070
- this._initialReviewCap = options.initialReviewCap;
14071
- }
14072
14198
  if (options?.outcomeObservers?.length) {
14073
14199
  this._outcomeObservers = [...options.outcomeObservers];
14074
14200
  }
14075
14201
  this.log(`Session constructed:
14076
14202
  startTime: ${this.startTime}
14077
14203
  endTime: ${this.endTime}
14078
- defaultBatchLimit: ${this._defaultBatchLimit}
14079
- initialReviewCap: ${this._initialReviewCap}`);
14204
+ defaultBatchLimit: ${this._defaultBatchLimit}`);
14080
14205
  registerActiveController(this);
14081
14206
  }
14082
14207
  tick() {
@@ -14110,15 +14235,6 @@ var SessionController = class _SessionController extends Loggable {
14110
14235
  this.log(`Failed card cleanup estimate: ${Math.round(ret)}`);
14111
14236
  return ret;
14112
14237
  }
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
14238
  async prepareSession() {
14123
14239
  if (this.sources.some((s) => typeof s.getWeightedCards !== "function")) {
14124
14240
  throw new Error(
@@ -14133,15 +14249,15 @@ var SessionController = class _SessionController extends Loggable {
14133
14249
  );
14134
14250
  }
14135
14251
  await this.hydrationService.ensureHydratedCards();
14136
- startSessionTracking(this.reviewQ.length, this.newQ.length, this.failedQ.length);
14252
+ startSessionTracking(this.supplyQ.length, this.failedQ.length);
14137
14253
  this._intervalHandle = setInterval(() => {
14138
14254
  this.tick();
14139
14255
  }, 1e3);
14140
14256
  }
14141
14257
  /**
14142
14258
  * 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.
14259
+ * and atomically replaces (or merges into) the supplyQ contents. Safe to call
14260
+ * at any time during a session.
14145
14261
  *
14146
14262
  * Concurrency policy:
14147
14263
  * - Two unhinted auto-replans never run in parallel; the second coalesces
@@ -14155,7 +14271,8 @@ var SessionController = class _SessionController extends Loggable {
14155
14271
  * results (e.g. surfacing another gpc-intro card right after one
14156
14272
  * completed, skipping the prescribed `c-wst-*` follow-up).
14157
14273
  *
14158
- * Does NOT affect reviewQ or failedQ.
14274
+ * Re-pulls and re-ranks the whole supply (including reviews); does NOT affect
14275
+ * failedQ (controller-owned remediation).
14159
14276
  *
14160
14277
  * If nextCard() is called while a replan is in flight, it will automatically
14161
14278
  * await the replan before drawing from queues, ensuring the user always sees
@@ -14221,7 +14338,7 @@ var SessionController = class _SessionController extends Loggable {
14221
14338
  * excludeCards happen at *invocation* time, not at *queue* time. For a
14222
14339
  * queued replan that means excludes reflect the state after the prior
14223
14340
  * replan landed — which is what we want, since the prior replan's
14224
- * newQ.peek(0) is the imminent draw we need to exclude.
14341
+ * supplyQ.peek(0) is the imminent draw we need to exclude.
14225
14342
  */
14226
14343
  async _runReplan(opts) {
14227
14344
  this._activeReplanLabel = opts.label ?? "(auto)";
@@ -14234,8 +14351,8 @@ var SessionController = class _SessionController extends Loggable {
14234
14351
  for (const rec of this._sessionRecord) {
14235
14352
  excludeSet.add(rec.card.card_id);
14236
14353
  }
14237
- if (this.newQ.length > 0) {
14238
- excludeSet.add(this.newQ.peek(0).cardID);
14354
+ if (this.supplyQ.length > 0) {
14355
+ excludeSet.add(this.supplyQ.peek(0).cardID);
14239
14356
  }
14240
14357
  hints.excludeCards = [...excludeSet];
14241
14358
  if (opts.sessionHints !== void 0) {
@@ -14296,7 +14413,13 @@ var SessionController = class _SessionController extends Loggable {
14296
14413
  const describe = (q) => {
14297
14414
  const cards = [];
14298
14415
  for (let i = 0; i < q.length; i++) {
14299
- cards.push(q.peek(i).cardID);
14416
+ const item = q.peek(i);
14417
+ cards.push({
14418
+ cardID: item.cardID,
14419
+ status: item.status,
14420
+ origin: isReview(item) ? "review" : "new",
14421
+ score: item.score
14422
+ });
14300
14423
  }
14301
14424
  return { length: q.length, dequeueCount: q.dequeueCount, cards };
14302
14425
  };
@@ -14319,9 +14442,9 @@ var SessionController = class _SessionController extends Loggable {
14319
14442
  sessionHints: this._sessionHints,
14320
14443
  replanActive: this._replanPromise !== null,
14321
14444
  replanLabel: this._activeReplanLabel,
14322
- reviewQ: describe(this.reviewQ),
14323
- newQ: describe(this.newQ),
14445
+ supplyQ: describe(this.supplyQ),
14324
14446
  failedQ: describe(this.failedQ),
14447
+ reviewBacklog: getSrsBacklogDebug(),
14325
14448
  drawnCards
14326
14449
  };
14327
14450
  }
@@ -14441,7 +14564,7 @@ var SessionController = class _SessionController extends Loggable {
14441
14564
  */
14442
14565
  static WELL_INDICATED_SCORE = 0.1;
14443
14566
  /**
14444
- * newQ length at or below which the opportunistic depletion-prefetch
14567
+ * supplyQ length at or below which the opportunistic depletion-prefetch
14445
14568
  * fires. Sets the lead time available for the background replan to land
14446
14569
  * before the user actually empties the queue and falls into the
14447
14570
  * (synchronous) wedge-breaker path.
@@ -14454,7 +14577,7 @@ var SessionController = class _SessionController extends Loggable {
14454
14577
  */
14455
14578
  static DEPLETION_PREFETCH_THRESHOLD = 3;
14456
14579
  /**
14457
- * Internal replan execution. Runs the pipeline, builds a new newQ,
14580
+ * Internal replan execution. Runs the pipeline, rebuilds the supplyQ,
14458
14581
  * atomically swaps it in, and triggers hydration for the new contents.
14459
14582
  *
14460
14583
  * If the initial replan produces fewer than MIN_WELL_INDICATED cards that
@@ -14483,8 +14606,8 @@ var SessionController = class _SessionController extends Loggable {
14483
14606
  }
14484
14607
  await this.hydrationService.ensureHydratedCards();
14485
14608
  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);
14609
+ this.log(`Replan complete${labelTag}: supplyQ now has ${this.supplyQ.length} cards (mode=${mode})`);
14610
+ snapshotQueues(this.supplyQ.length, this.failedQ.length);
14488
14611
  }
14489
14612
  addTime(seconds) {
14490
14613
  this.endTime = new Date(this.endTime.valueOf() + 1e3 * seconds);
@@ -14493,10 +14616,10 @@ var SessionController = class _SessionController extends Loggable {
14493
14616
  return this.failedQ.length;
14494
14617
  }
14495
14618
  toString() {
14496
- return `Session: ${this.reviewQ.length} Reviews, ${this.newQ.length} New, ${this.failedQ.length} failed`;
14619
+ return `Session: ${this.supplyQ.length} supply, ${this.failedQ.length} failed`;
14497
14620
  }
14498
14621
  reportString() {
14499
- return `${this.reviewQ.dequeueCount} Reviews, ${this.newQ.dequeueCount} New, ${this.failedQ.dequeueCount} failed`;
14622
+ return `${this.supplyQ.dequeueCount} supply, ${this.failedQ.dequeueCount} failed`;
14500
14623
  }
14501
14624
  /**
14502
14625
  * Returns debug information about the current session state.
@@ -14513,7 +14636,8 @@ var SessionController = class _SessionController extends Loggable {
14513
14636
  items.push({
14514
14637
  courseID: item.courseID || "unknown",
14515
14638
  cardID: item.cardID || "unknown",
14516
- status: item.status || "unknown"
14639
+ status: item.status || "unknown",
14640
+ score: item.score
14517
14641
  });
14518
14642
  }
14519
14643
  return items;
@@ -14523,15 +14647,10 @@ var SessionController = class _SessionController extends Loggable {
14523
14647
  mode: supportsWeightedCards ? "weighted" : "legacy",
14524
14648
  description: supportsWeightedCards ? "Using getWeightedCards() API with scored candidates" : "ERROR: getWeightedCards() not a function."
14525
14649
  },
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)
14650
+ supplyQueue: {
14651
+ length: this.supplyQ.length,
14652
+ dequeueCount: this.supplyQ.dequeueCount,
14653
+ items: extractQueueItems(this.supplyQ)
14535
14654
  },
14536
14655
  failedQueue: {
14537
14656
  length: this.failedQ.length,
@@ -14551,30 +14670,29 @@ var SessionController = class _SessionController extends Loggable {
14551
14670
  };
14552
14671
  }
14553
14672
  /**
14554
- * Fetch content using the getWeightedCards API and mix across sources.
14673
+ * Fetch weighted content from all sources, mix across sources, and populate
14674
+ * the single supply queue in pipeline rank order.
14555
14675
  *
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.
14676
+ * Reviews and new cards compete on one cross-comparable scale (SRS 0.5–1.0
14677
+ * w/ backlog pressure vs ELO 0.0–1.0) there is no origin split and no
14678
+ * second mixer. The working set is `supplyLimit` cards (the top of the mixed
14679
+ * ranking, plus any `+INF` required cards floated to the front); replans
14680
+ * re-pull and re-rank the whole supply, so a heavy review backlog surfaces as
14681
+ * a refreshed top-ranked working set rather than a frozen 200-card snapshot.
14564
14682
  *
14565
14683
  * @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.
14684
+ * initial session setup. Atomically replaces supplyQ contents and treats
14685
+ * empty results as non-fatal.
14686
+ * @param options.additive - If true (replan only), merge high-quality
14687
+ * candidates into the front of the existing supplyQ instead of replacing it.
14570
14688
  * @returns Number of "well-indicated" cards (passed all hierarchy filters)
14571
14689
  * in the new content. Returns -1 if no content was loaded.
14572
14690
  */
14573
14691
  async getWeightedContent(options) {
14574
14692
  const replan = options?.replan ?? false;
14575
14693
  const additive = options?.additive ?? false;
14576
- const newLimit = options?.limit ?? this._defaultBatchLimit;
14577
- const fetchLimit = replan ? newLimit : newLimit + this._initialReviewCap;
14694
+ const supplyLimit = options?.limit ?? this._defaultBatchLimit;
14695
+ const fetchLimit = supplyLimit;
14578
14696
  if (!replan) {
14579
14697
  this._applyHintsToSources();
14580
14698
  }
@@ -14596,7 +14714,7 @@ var SessionController = class _SessionController extends Loggable {
14596
14714
  }
14597
14715
  if (batches.length === 0) {
14598
14716
  if (replan) {
14599
- this.log("Replan: no content from any source, keeping existing newQ");
14717
+ this.log("Replan: no content from any source, keeping existing supplyQ");
14600
14718
  return -1;
14601
14719
  }
14602
14720
  throw new Error(
@@ -14630,64 +14748,59 @@ var SessionController = class _SessionController extends Loggable {
14630
14748
  quotaPerSource,
14631
14749
  mixedWeighted
14632
14750
  );
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 = [
14751
+ const candidates = mixedWeighted.filter((w) => !this._servedCardIds.has(w.cardId));
14752
+ const mandatoryWeighted = candidates.filter((w) => w.score === Number.POSITIVE_INFINITY);
14753
+ const optionalWeighted = candidates.filter((w) => w.score !== Number.POSITIVE_INFINITY);
14754
+ const supplyWeighted = [
14640
14755
  ...mandatoryWeighted,
14641
- ...optionalWeighted.slice(0, Math.max(0, newLimit - mandatoryWeighted.length))
14756
+ ...optionalWeighted.slice(0, Math.max(0, supplyLimit - mandatoryWeighted.length))
14642
14757
  ];
14643
14758
  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(
14759
+ const wellIndicated = supplyWeighted.filter(
14662
14760
  (w) => w.score >= _SessionController.WELL_INDICATED_SCORE
14663
14761
  ).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)})
14762
+ let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
14763
+ const supplyItems = supplyWeighted.map((w) => {
14764
+ const origin = getCardOrigin(w);
14765
+ const scoreStr = Number.isFinite(w.score) ? w.score.toFixed(2) : "+INF";
14766
+ report += `${origin === "review" ? "Review" : "New"}: ${w.courseId}::${w.cardId} (score: ${scoreStr})
14675
14767
  `;
14676
- }
14768
+ return this._buildSupplyItem(w, origin);
14769
+ });
14677
14770
  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
14771
+ const added = this.supplyQ.mergeToFront(supplyItems, (item) => item.cardID, mandatoryIds);
14772
+ report += `Additive merge: ${added} cards added to front of supplyQ
14680
14773
  `;
14681
14774
  } else if (replan) {
14682
- this.newQ.replaceAll(newItems, (item) => item.cardID);
14775
+ this.supplyQ.replaceAll(supplyItems, (item) => item.cardID);
14683
14776
  } else {
14684
- for (const item of newItems) {
14685
- this.newQ.add(item, item.cardID);
14777
+ for (const item of supplyItems) {
14778
+ this.supplyQ.add(item, item.cardID);
14686
14779
  }
14687
14780
  }
14688
14781
  this.log(report);
14689
14782
  return wellIndicated;
14690
14783
  }
14784
+ /**
14785
+ * Build a supply item from a weighted candidate. Review-origin cards carry
14786
+ * their `reviewID` so SRS outcome tracking and re-presentation work; new
14787
+ * cards do not. `score` is carried on both for the debug overlay.
14788
+ */
14789
+ _buildSupplyItem(w, origin = getCardOrigin(w)) {
14790
+ const base = {
14791
+ cardID: w.cardId,
14792
+ courseID: w.courseId,
14793
+ contentSourceType: "course",
14794
+ contentSourceID: w.courseId,
14795
+ score: w.score
14796
+ };
14797
+ if (origin === "review") {
14798
+ const reviewItem = { ...base, status: "review", reviewID: w.reviewID };
14799
+ return reviewItem;
14800
+ }
14801
+ const newItem = { ...base, status: "new" };
14802
+ return newItem;
14803
+ }
14691
14804
  /**
14692
14805
  * Returns items that should be pre-hydrated.
14693
14806
  * Deterministic: top N items from each queue to ensure coverage.
@@ -14695,71 +14808,73 @@ var SessionController = class _SessionController extends Loggable {
14695
14808
  */
14696
14809
  _getItemsToHydrate() {
14697
14810
  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));
14811
+ const SUPPLY_PREFETCH = 3;
14812
+ const FAILED_PREFETCH = 2;
14813
+ for (let i = 0; i < Math.min(SUPPLY_PREFETCH, this.supplyQ.length); i++) {
14814
+ items.push(this.supplyQ.peek(i));
14704
14815
  }
14705
- for (let i = 0; i < Math.min(ITEMS_PER_QUEUE, this.failedQ.length); i++) {
14816
+ for (let i = 0; i < Math.min(FAILED_PREFETCH, this.failedQ.length); i++) {
14706
14817
  items.push(this.failedQ.peek(i));
14707
14818
  }
14708
14819
  return items;
14709
14820
  }
14710
14821
  /**
14711
14822
  * Selects the next item to present to the user.
14712
- * Nondeterministic: uses probability to balance between queues based on session state.
14823
+ *
14824
+ * The supplyQ is already rank-ordered (the pipeline + mixer did the mixing,
14825
+ * with `+INF` required cards floated to the front), so the primary path is a
14826
+ * deterministic front-to-back draw — no second new-vs-review mixer. The only
14827
+ * remaining decisions are (a) when the session ends and (b) when to interleave
14828
+ * a remediation card from failedQ. See decision doc §2/§3/§7.
14713
14829
  */
14714
14830
  _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) {
14831
+ if (this.supplyQ.length === 0 && this.failedQ.length === 0) {
14719
14832
  return null;
14720
14833
  }
14721
14834
  if (this._secondsRemaining < 2 && this.failedQ.length === 0 && this._minCardsGuarantee <= 0) {
14722
14835
  return null;
14723
14836
  }
14724
14837
  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);
14838
+ return this.failedQ.length > 0 ? this.failedQ.peek(0) : null;
14733
14839
  }
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;
14840
+ const supplyTop = this.supplyQ.length > 0 ? this.supplyQ.peek(0) : null;
14841
+ if (this._minCardsGuarantee > 0 && supplyTop) {
14842
+ return supplyTop;
14746
14843
  }
14747
- if (this.failedQ.length === 0) {
14748
- reviewBound = 1;
14844
+ if (this.failedQ.length > 0 && this._shouldInterleaveFailed(supplyTop !== null)) {
14845
+ return this.failedQ.peek(0);
14749
14846
  }
14750
- if (this.reviewQ.length === 0) {
14751
- newBound = reviewBound;
14847
+ if (supplyTop) {
14848
+ return supplyTop;
14752
14849
  }
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) {
14850
+ if (this.failedQ.length > 0) {
14758
14851
  return this.failedQ.peek(0);
14759
- } else {
14760
- this.log(`No more cards available for the session!`);
14761
- return null;
14762
14852
  }
14853
+ this.log(`No more cards available for the session!`);
14854
+ return null;
14855
+ }
14856
+ /** Supply draws between forced failed-queue interleaves (light steady cadence). */
14857
+ static FAILED_INTERLEAVE_EVERY = 4;
14858
+ /**
14859
+ * Slack (seconds) below which the endgame failed-pressure kicks in: when the
14860
+ * time left after clearing remediation drops under this, bias hard to failed
14861
+ * so the session doesn't end with un-cleared remediation. Mirrors the old
14862
+ * `availableTime > 20` ladder thresholds.
14863
+ */
14864
+ static FAILED_ENDGAME_SLACK_SECONDS = 20;
14865
+ /**
14866
+ * Whether to interleave a failed (remediation) card now instead of drawing
14867
+ * the supply head. Replaces the old `newBound`/`reviewBound` probability
14868
+ * ladder's failed path (decision doc §7).
14869
+ *
14870
+ * @param supplyAvailable - whether supplyQ has a card to draw instead.
14871
+ */
14872
+ _shouldInterleaveFailed(supplyAvailable) {
14873
+ if (this.failedQ.length === 0) return false;
14874
+ if (!supplyAvailable) return true;
14875
+ const availableTime = this._secondsRemaining - this.estimateCleanupTime();
14876
+ if (availableTime <= _SessionController.FAILED_ENDGAME_SLACK_SECONDS) return true;
14877
+ return this._supplyDrawsSinceFailed >= _SessionController.FAILED_INTERLEAVE_EVERY;
14763
14878
  }
14764
14879
  async nextCard(action = "dismiss-success") {
14765
14880
  this.dismissCurrentCard(action);
@@ -14767,22 +14882,21 @@ var SessionController = class _SessionController extends Loggable {
14767
14882
  this._minCardsGuarantee--;
14768
14883
  this.log(`[CardGuarantee] ${this._minCardsGuarantee} guaranteed cards remaining`);
14769
14884
  }
14770
- if (this._replanPromise && this.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
14885
+ if (this._replanPromise && this.supplyQ.length === 0 && this.failedQ.length === 0) {
14771
14886
  this.log("nextCard: queues empty, awaiting in-flight replan before drawing");
14772
14887
  await this._replanPromise;
14773
14888
  }
14774
- if (this.newQ.length <= _SessionController.DEPLETION_PREFETCH_THRESHOLD && this._secondsRemaining > 0 && !this._replanPromise) {
14889
+ if (this.supplyQ.length <= _SessionController.DEPLETION_PREFETCH_THRESHOLD && this._secondsRemaining > 0 && !this._replanPromise) {
14775
14890
  this._suppressQualityReplan = false;
14776
- const otherContent = this.reviewQ.length + this.failedQ.length;
14777
14891
  this.log(
14778
- `[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
14892
+ `[AutoReplan:depletion] supplyQ has ${this.supplyQ.length} card(s) (${this.failedQ.length} failed pending) with ${this._secondsRemaining}s remaining. Triggering background replan.`
14779
14893
  );
14780
14894
  void this.requestReplan({ label: "auto:depletion", mode: "merge" });
14781
14895
  }
14782
14896
  const REPLAN_BUFFER = 3;
14783
- if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
14897
+ if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.supplyQ.length > 0 && !this._replanPromise) {
14784
14898
  this.log(
14785
- `[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (newQ: ${this.newQ.length}). Triggering background replan.`
14899
+ `[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (supplyQ: ${this.supplyQ.length}). Triggering background replan.`
14786
14900
  );
14787
14901
  void this.requestReplan({ label: "auto:quality" });
14788
14902
  }
@@ -14794,12 +14908,12 @@ var SessionController = class _SessionController extends Loggable {
14794
14908
  const WEDGE_MAX_EMPTY_STREAK = 3;
14795
14909
  const WEDGE_BACKOFF_MS = 250;
14796
14910
  let wedgeEmptyStreak = 0;
14797
- while (this._secondsRemaining > 0 && this.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
14911
+ while (this._secondsRemaining > 0 && this.supplyQ.length === 0 && this.failedQ.length === 0) {
14798
14912
  this.log(
14799
14913
  `[WedgeBreaker] All queues empty with ${this._secondsRemaining}s remaining. Running pipeline (attempt ${wedgeEmptyStreak + 1}/${WEDGE_MAX_EMPTY_STREAK}).`
14800
14914
  );
14801
14915
  await this._replanUncoalesced({ label: "wedge-breaker" });
14802
- if (this.newQ.length === 0 && this.reviewQ.length === 0 && this.failedQ.length === 0) {
14916
+ if (this.supplyQ.length === 0 && this.failedQ.length === 0) {
14803
14917
  wedgeEmptyStreak++;
14804
14918
  if (wedgeEmptyStreak >= WEDGE_MAX_EMPTY_STREAK) {
14805
14919
  this.log(
@@ -14829,15 +14943,16 @@ var SessionController = class _SessionController extends Loggable {
14829
14943
  await this.hydrationService.ensureHydratedCards();
14830
14944
  this._currentCard = card;
14831
14945
  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";
14946
+ const queueSource = nextItem.status.startsWith("failed") ? "failedQ" : "supplyQ";
14833
14947
  recordCardPresentation(
14834
14948
  nextItem.cardID,
14835
14949
  nextItem.courseID,
14836
14950
  this.courseNameCache.get(nextItem.courseID),
14837
14951
  origin,
14838
- queueSource
14952
+ queueSource,
14953
+ nextItem.score
14839
14954
  );
14840
- snapshotQueues(this.reviewQ.length, this.newQ.length, this.failedQ.length);
14955
+ snapshotQueues(this.supplyQ.length, this.failedQ.length);
14841
14956
  return card;
14842
14957
  }
14843
14958
  this.log(`Skipping card ${nextItem.cardID}: hydration failed, trying next`);
@@ -14907,6 +15022,7 @@ var SessionController = class _SessionController extends Loggable {
14907
15022
  };
14908
15023
  }
14909
15024
  this.failedQ.add(failedItem, failedItem.cardID);
15025
+ this._supplyDrawsSinceFailed = 0;
14910
15026
  } else if (action === "dismiss-error") {
14911
15027
  this.hydrationService.removeCard(this._currentCard.item.cardID);
14912
15028
  } else if (action === "dismiss-failed") {
@@ -14920,15 +15036,15 @@ var SessionController = class _SessionController extends Loggable {
14920
15036
  removeItemFromQueue(item) {
14921
15037
  this._clearDurableRequirement(item.cardID);
14922
15038
  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);
15039
+ if (this.failedQ.peek(0)?.cardID === item.cardID) {
15040
+ this.failedQ.dequeue((queueItem) => queueItem.cardID);
15041
+ this._supplyDrawsSinceFailed = 0;
15042
+ } else if (this.supplyQ.peek(0)?.cardID === item.cardID) {
15043
+ this.supplyQ.dequeue((queueItem) => queueItem.cardID);
15044
+ this._supplyDrawsSinceFailed++;
14927
15045
  if (this._wellIndicatedRemaining > 0) {
14928
15046
  this._wellIndicatedRemaining--;
14929
15047
  }
14930
- } else if (this.failedQ.peek(0)?.cardID === item.cardID) {
14931
- this.failedQ.dequeue((queueItem) => queueItem.cardID);
14932
15048
  }
14933
15049
  }
14934
15050
  /**
@@ -15027,6 +15143,7 @@ export {
15027
15143
  areQuestionRecords,
15028
15144
  buildStrategyStateId,
15029
15145
  captureMixerRun,
15146
+ clearSrsBacklogDebug,
15030
15147
  computeDeviation,
15031
15148
  computeEffectiveWeight,
15032
15149
  computeOutcomeSignal,
@@ -15047,6 +15164,7 @@ export {
15047
15164
  getRegisteredNavigator,
15048
15165
  getRegisteredNavigatorNames,
15049
15166
  getRegisteredNavigatorRole,
15167
+ getSrsBacklogDebug,
15050
15168
  getStudySource,
15051
15169
  hasRegisteredNavigator,
15052
15170
  importParsedCards,