@vue-skuilder/db 0.2.7 → 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 (40) 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 +67 -4
  4. package/dist/core/index.d.ts +67 -4
  5. package/dist/core/index.js +201 -39
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +198 -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 +195 -39
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +195 -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 +195 -39
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +195 -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 +440 -251
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +437 -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/Pipeline.ts +93 -1
  33. package/src/core/navigators/PipelineDebugger.ts +11 -1
  34. package/src/core/navigators/SrsDebugger.ts +53 -0
  35. package/src/core/navigators/generators/prescribed.ts +76 -9
  36. package/src/core/navigators/generators/srs.ts +81 -37
  37. package/src/core/navigators/index.ts +9 -0
  38. package/src/study/SessionController.ts +260 -249
  39. package/src/study/SessionDebugger.ts +15 -25
  40. package/src/study/SessionOverlay.ts +108 -13
@@ -814,6 +814,7 @@ __export(PipelineDebugger_exports, {
814
814
  buildRunReport: () => buildRunReport,
815
815
  captureRun: () => captureRun,
816
816
  clearRunHistory: () => clearRunHistory,
817
+ getActivePipeline: () => getActivePipeline,
817
818
  mountPipelineDebugger: () => mountPipelineDebugger,
818
819
  pipelineDebugAPI: () => pipelineDebugAPI,
819
820
  registerPipelineForDebug: () => registerPipelineForDebug
@@ -821,6 +822,9 @@ __export(PipelineDebugger_exports, {
821
822
  function registerPipelineForDebug(pipeline) {
822
823
  _activePipeline = pipeline;
823
824
  }
825
+ function getActivePipeline() {
826
+ return _activePipeline;
827
+ }
824
828
  function clearRunHistory() {
825
829
  runHistory.length = 0;
826
830
  }
@@ -1636,6 +1640,30 @@ Example:
1636
1640
  }
1637
1641
  });
1638
1642
 
1643
+ // src/core/navigators/SrsDebugger.ts
1644
+ var SrsDebugger_exports = {};
1645
+ __export(SrsDebugger_exports, {
1646
+ captureSrsBacklog: () => captureSrsBacklog,
1647
+ clearSrsBacklogDebug: () => clearSrsBacklogDebug,
1648
+ getSrsBacklogDebug: () => getSrsBacklogDebug
1649
+ });
1650
+ function captureSrsBacklog(snapshot) {
1651
+ snapshots.set(snapshot.courseId, snapshot);
1652
+ }
1653
+ function getSrsBacklogDebug() {
1654
+ return [...snapshots.values()].sort((a, b) => b.timestamp - a.timestamp);
1655
+ }
1656
+ function clearSrsBacklogDebug() {
1657
+ snapshots.clear();
1658
+ }
1659
+ var snapshots;
1660
+ var init_SrsDebugger = __esm({
1661
+ "src/core/navigators/SrsDebugger.ts"() {
1662
+ "use strict";
1663
+ snapshots = /* @__PURE__ */ new Map();
1664
+ }
1665
+ });
1666
+
1639
1667
  // src/core/navigators/generators/CompositeGenerator.ts
1640
1668
  var CompositeGenerator_exports = {};
1641
1669
  __export(CompositeGenerator_exports, {
@@ -2003,7 +2031,7 @@ function shuffleInPlace(arr) {
2003
2031
  function pickTopByScore(cards, limit) {
2004
2032
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
2005
2033
  }
2006
- 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;
2034
+ 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;
2007
2035
  var init_prescribed = __esm({
2008
2036
  "src/core/navigators/generators/prescribed.ts"() {
2009
2037
  "use strict";
@@ -2020,6 +2048,9 @@ var init_prescribed = __esm({
2020
2048
  BASE_SUPPORT_SCORE = 0.8;
2021
2049
  DISCOVERED_SUPPORT_SCORE = 12;
2022
2050
  BASE_PRACTICE_SCORE = 1;
2051
+ PRACTICE_BASE_MULT = 2;
2052
+ MAX_PRACTICE_MULTIPLIER = 4;
2053
+ PRACTICE_STALENESS_BUMP_PER_DAY = 0.5;
2023
2054
  MAX_TARGET_MULTIPLIER = 8;
2024
2055
  MAX_SUPPORT_MULTIPLIER = 4;
2025
2056
  PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
@@ -2079,6 +2110,8 @@ var init_prescribed = __esm({
2079
2110
  const emitted = [];
2080
2111
  const emittedIds = /* @__PURE__ */ new Set();
2081
2112
  const groupRuntimes = [];
2113
+ const priorPracticeDebt = progress.practiceDebt ?? {};
2114
+ const nextPracticeDebt = {};
2082
2115
  for (const group of this.config.groups) {
2083
2116
  const runtime = this.buildGroupRuntimeState({
2084
2117
  group,
@@ -2136,10 +2169,13 @@ var init_prescribed = __esm({
2136
2169
  userTagElo,
2137
2170
  userGlobalElo,
2138
2171
  activeIds,
2139
- seenIds
2172
+ seenIds,
2173
+ priorPracticeDebt,
2174
+ nextPracticeDebt
2140
2175
  });
2141
2176
  emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
2142
2177
  }
2178
+ nextState.practiceDebt = nextPracticeDebt;
2143
2179
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
2144
2180
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
2145
2181
  boostTags: hintSummary.boostTags,
@@ -2473,9 +2509,16 @@ var init_prescribed = __esm({
2473
2509
  * `practiceMinCount`), this resolves cards carrying that tag and emits them
2474
2510
  * into the candidate pool. It exists because global-ELO retrieval
2475
2511
  * systematically fails to fetch the (low-ELO) drill cards for a
2476
- * freshly-introduced skill — putting them in the pool here lets the pipeline's
2477
- * scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
2478
- * this method's job; it only guarantees presence.
2512
+ * freshly-introduced skill — putting them in the pool here guarantees presence.
2513
+ *
2514
+ * Emphasis is a **practice-debt pressure** (parallel to SRS backlog pressure):
2515
+ * cards score `base × multiplier`, where the multiplier starts at
2516
+ * PRACTICE_BASE_MULT (so a few reps land promptly post-intro, competing with
2517
+ * pressured reviews) and escalates by how long the debt has stayed open
2518
+ * (per-tag, time-based via `priorPracticeDebt`/`nextPracticeDebt`), clamped at
2519
+ * MAX_PRACTICE_MULTIPLIER. The debt is durable and self-discharges the instant
2520
+ * the skill reaches `practiceMinCount` — so this no longer relies on the
2521
+ * session-scoped intro boost to actually surface.
2479
2522
  *
2480
2523
  * Fully data-driven: the unlock relation comes from the hierarchy config and
2481
2524
  * practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
@@ -2490,7 +2533,9 @@ var init_prescribed = __esm({
2490
2533
  userTagElo,
2491
2534
  userGlobalElo,
2492
2535
  activeIds,
2493
- seenIds
2536
+ seenIds,
2537
+ priorPracticeDebt,
2538
+ nextPracticeDebt
2494
2539
  } = args;
2495
2540
  const patterns = group.practiceTagPatterns ?? [];
2496
2541
  if (patterns.length === 0) return [];
@@ -2500,6 +2545,20 @@ var init_prescribed = __esm({
2500
2545
  (tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
2501
2546
  );
2502
2547
  if (practiceTags.length === 0) return [];
2548
+ const now = Date.now();
2549
+ const DAY_MS = 24 * 60 * 60 * 1e3;
2550
+ const tagMultiplier = /* @__PURE__ */ new Map();
2551
+ for (const tag of practiceTags) {
2552
+ const firstOwedAt = priorPracticeDebt[tag] ?? isoNow();
2553
+ nextPracticeDebt[tag] = firstOwedAt;
2554
+ const staleDays = Math.max(0, (now - new Date(firstOwedAt).getTime()) / DAY_MS);
2555
+ const mult = clamp(
2556
+ PRACTICE_BASE_MULT + staleDays * PRACTICE_STALENESS_BUMP_PER_DAY,
2557
+ PRACTICE_BASE_MULT,
2558
+ MAX_PRACTICE_MULTIPLIER
2559
+ );
2560
+ tagMultiplier.set(tag, mult);
2561
+ }
2503
2562
  const practiceCardIds = this.findDiscoveredSupportCards({
2504
2563
  supportTags: practiceTags,
2505
2564
  cardsByTag,
@@ -2515,18 +2574,25 @@ var init_prescribed = __esm({
2515
2574
  const cards = [];
2516
2575
  for (const cardId of practiceCardIds) {
2517
2576
  emittedIds.add(cardId);
2577
+ let mult = PRACTICE_BASE_MULT;
2578
+ for (const tag of practiceTags) {
2579
+ if (cardsByTag.get(tag)?.includes(cardId) ?? false) {
2580
+ mult = Math.max(mult, tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT);
2581
+ }
2582
+ }
2583
+ const score = BASE_PRACTICE_SCORE * mult;
2518
2584
  cards.push({
2519
2585
  cardId,
2520
2586
  courseId,
2521
- score: BASE_PRACTICE_SCORE,
2587
+ score,
2522
2588
  provenance: [
2523
2589
  {
2524
2590
  strategy: "prescribed",
2525
2591
  strategyName: this.strategyName || this.name,
2526
2592
  strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2527
2593
  action: "generated",
2528
- score: BASE_PRACTICE_SCORE,
2529
- reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
2594
+ score,
2595
+ 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}`
2530
2596
  }
2531
2597
  ]
2532
2598
  });
@@ -2699,15 +2765,16 @@ var srs_exports = {};
2699
2765
  __export(srs_exports, {
2700
2766
  default: () => SRSNavigator
2701
2767
  });
2702
- var import_moment3, DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_PRESSURE, SRSNavigator;
2768
+ var import_moment3, DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_MULTIPLIER, SRSNavigator;
2703
2769
  var init_srs = __esm({
2704
2770
  "src/core/navigators/generators/srs.ts"() {
2705
2771
  "use strict";
2706
2772
  import_moment3 = __toESM(require("moment"), 1);
2707
2773
  init_navigators();
2774
+ init_SrsDebugger();
2708
2775
  init_logger();
2709
2776
  DEFAULT_HEALTHY_BACKLOG = 20;
2710
- MAX_BACKLOG_PRESSURE = 0.5;
2777
+ MAX_BACKLOG_MULTIPLIER = 2;
2711
2778
  SRSNavigator = class extends ContentNavigator {
2712
2779
  /** Human-readable name for CardGenerator interface */
2713
2780
  name;
@@ -2774,9 +2841,18 @@ var init_srs = __esm({
2774
2841
  }
2775
2842
  }
2776
2843
  }
2777
- const backlogPressure = this.computeBacklogPressure(dueReviews.length);
2844
+ const backlogMultiplier = this.computeBacklogMultiplier(dueReviews.length);
2845
+ const notDue = reviews.filter((r) => !now.isAfter(import_moment3.default.utc(r.reviewTime)));
2846
+ let nextDueIn = null;
2847
+ if (notDue.length > 0) {
2848
+ const next = notDue.reduce(
2849
+ (a, b) => import_moment3.default.utc(a.reviewTime).isBefore(import_moment3.default.utc(b.reviewTime)) ? a : b
2850
+ );
2851
+ const until = import_moment3.default.duration(import_moment3.default.utc(next.reviewTime).diff(now));
2852
+ nextDueIn = until.asHours() < 1 ? `${Math.round(until.asMinutes())}m` : until.asHours() < 24 ? `${Math.round(until.asHours())}h` : `${Math.round(until.asDays())}d`;
2853
+ }
2778
2854
  if (dueReviews.length > 0) {
2779
- const pressureNote = backlogPressure > 0 ? ` [backlog pressure: +${backlogPressure.toFixed(2)}]` : ` [healthy backlog]`;
2855
+ const pressureNote = backlogMultiplier > 1 ? ` [backlog pressure: \xD7${backlogMultiplier.toFixed(2)}]` : ` [healthy backlog]`;
2780
2856
  logger.info(
2781
2857
  `[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
2782
2858
  );
@@ -2795,7 +2871,7 @@ var init_srs = __esm({
2795
2871
  logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
2796
2872
  }
2797
2873
  const scored = dueReviews.map((review) => {
2798
- const { score, reason } = this.computeUrgencyScore(review, now, backlogPressure);
2874
+ const { score, reason } = this.computeUrgencyScore(review, now, backlogMultiplier);
2799
2875
  return {
2800
2876
  cardId: review.cardId,
2801
2877
  courseId: review.courseId,
@@ -2813,30 +2889,42 @@ var init_srs = __esm({
2813
2889
  ]
2814
2890
  };
2815
2891
  });
2816
- return { cards: scored.sort((a, b) => b.score - a.score).slice(0, limit) };
2892
+ const sorted = scored.sort((a, b) => b.score - a.score);
2893
+ captureSrsBacklog({
2894
+ courseId,
2895
+ scheduledTotal: reviews.length,
2896
+ dueNow: dueReviews.length,
2897
+ healthyBacklog: this.healthyBacklog,
2898
+ backlogMultiplier,
2899
+ maxBacklogMultiplier: MAX_BACKLOG_MULTIPLIER,
2900
+ topReviewScore: sorted.length > 0 ? sorted[0].score : null,
2901
+ nextDueIn,
2902
+ timestamp: Date.now()
2903
+ });
2904
+ return { cards: sorted.slice(0, limit) };
2817
2905
  }
2818
2906
  /**
2819
- * Compute backlog pressure based on number of due reviews.
2907
+ * Compute the multiplicative backlog pressure based on number of due reviews.
2820
2908
  *
2821
- * Backlog pressure is 0 when at or below healthy threshold,
2822
- * and increases linearly above it, maxing out at MAX_BACKLOG_PRESSURE.
2909
+ * ×1.0 at or below the healthy threshold (no boost), increasing linearly above
2910
+ * it and maxing out at MAX_BACKLOG_MULTIPLIER at the healthy backlog.
2823
2911
  *
2824
- * Examples (with default healthyBacklog=20):
2825
- * - 10 due reviews → 0.00 (healthy)
2826
- * - 20 due reviews → 0.00 (at threshold)
2827
- * - 40 due reviews → 0.25 (2x threshold)
2828
- * - 60 due reviews → 0.50 (3x threshold, maxed)
2912
+ * Examples (with default healthyBacklog=20, MAX_BACKLOG_MULTIPLIER=2.0):
2913
+ * - 10 due reviews → ×1.00 (healthy)
2914
+ * - 20 due reviews → ×1.00 (at threshold)
2915
+ * - 40 due reviews → ×1.50 (2x threshold)
2916
+ * - 60 due reviews → ×2.00 (3x threshold, maxed)
2829
2917
  *
2830
2918
  * @param dueCount - Number of reviews currently due
2831
- * @returns Backlog pressure score to add to urgency (0 to MAX_BACKLOG_PRESSURE)
2919
+ * @returns Multiplier applied to review urgency (1.0 to MAX_BACKLOG_MULTIPLIER)
2832
2920
  */
2833
- computeBacklogPressure(dueCount) {
2921
+ computeBacklogMultiplier(dueCount) {
2834
2922
  if (dueCount <= this.healthyBacklog) {
2835
- return 0;
2923
+ return 1;
2836
2924
  }
2837
2925
  const excess = dueCount - this.healthyBacklog;
2838
- const pressure = excess / this.healthyBacklog * (MAX_BACKLOG_PRESSURE / 2);
2839
- return Math.min(MAX_BACKLOG_PRESSURE, pressure);
2926
+ const multiplier = 1 + excess / this.healthyBacklog * ((MAX_BACKLOG_MULTIPLIER - 1) / 2);
2927
+ return Math.min(MAX_BACKLOG_MULTIPLIER, multiplier);
2840
2928
  }
2841
2929
  /**
2842
2930
  * Compute urgency score for a review card.
@@ -2851,19 +2939,20 @@ var init_srs = __esm({
2851
2939
  * - 30 days (720h) → ~0.56
2852
2940
  * - 180 days → ~0.30
2853
2941
  *
2854
- * 3. Backlog pressure = global boost when review backlog exceeds healthy threshold
2855
- * - At healthy backlog: 0
2856
- * - At 2x healthy: +0.25
2857
- * - At 3x+ healthy: +0.50 (max)
2942
+ * 3. Backlog pressure = global *multiplier* when review backlog exceeds the
2943
+ * healthy threshold (×1.0 healthy up to MAX_BACKLOG_MULTIPLIER at 3×).
2858
2944
  *
2859
- * Combined: base 0.5 + (urgency factors * 0.45) + backlog pressure
2860
- * Result range: 0.5 to 1.0 (uncapped to allow high-urgency reviews to compete with new cards)
2945
+ * Combined: (base 0.5 + urgency factors * 0.45) × backlog multiplier.
2946
+ * Per-card range before pressure: ~0.57–0.95. NOT clamped to 1.0 under a
2947
+ * heavy backlog reviews scale onto the open scale to compete with (and exceed)
2948
+ * new cards; what keeps them from running away is the bounded multiplier, not
2949
+ * a hard ceiling.
2861
2950
  *
2862
2951
  * @param review - The scheduled card to score
2863
2952
  * @param now - Current time
2864
- * @param backlogPressure - Pre-computed backlog pressure (0 to 0.5)
2953
+ * @param backlogMultiplier - Pre-computed backlog multiplier (1.0 to MAX_BACKLOG_MULTIPLIER)
2865
2954
  */
2866
- computeUrgencyScore(review, now, backlogPressure) {
2955
+ computeUrgencyScore(review, now, backlogMultiplier) {
2867
2956
  const scheduledAt = import_moment3.default.utc(review.scheduledAt);
2868
2957
  const due = import_moment3.default.utc(review.reviewTime);
2869
2958
  const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
@@ -2873,15 +2962,15 @@ var init_srs = __esm({
2873
2962
  const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
2874
2963
  const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
2875
2964
  const baseScore = 0.5 + urgency * 0.45;
2876
- const score = Math.min(1, baseScore + backlogPressure);
2965
+ const score = baseScore * backlogMultiplier;
2877
2966
  const reasonParts = [
2878
2967
  `${Math.round(hoursOverdue)}h overdue`,
2879
2968
  `interval: ${Math.round(intervalHours)}h`,
2880
2969
  `relative: ${relativeOverdue.toFixed(2)}`,
2881
2970
  `recency: ${recencyFactor.toFixed(2)}`
2882
2971
  ];
2883
- if (backlogPressure > 0) {
2884
- reasonParts.push(`backlog: +${backlogPressure.toFixed(2)}`);
2972
+ if (backlogMultiplier > 1) {
2973
+ reasonParts.push(`backlog: \xD7${backlogMultiplier.toFixed(2)}`);
2885
2974
  }
2886
2975
  reasonParts.push("review");
2887
2976
  const reason = reasonParts.join(", ");
@@ -4853,6 +4942,68 @@ var init_Pipeline = __esm({
4853
4942
  // ---------------------------------------------------------------------------
4854
4943
  // Card-space diagnostic
4855
4944
  // ---------------------------------------------------------------------------
4945
+ /**
4946
+ * Commit-free forecast: score the user's full card space through the filter
4947
+ * chain and return the cards that are currently *reachable* (score >=
4948
+ * threshold), optionally nudged by caller-supplied hints and/or restricted
4949
+ * to cards the user hasn't seen yet.
4950
+ *
4951
+ * This is a GENERIC primitive — it returns scored, tag-hydrated cards and
4952
+ * stops there. It has no knowledge of any particular tag convention; callers
4953
+ * decide what the surviving cards mean (e.g. filter to their own "intro"
4954
+ * tag family). Nothing is written and no session is started.
4955
+ *
4956
+ * The optional `hints` are the "out-of-band kick": they run through the same
4957
+ * {@link applyHints} path a live replan uses, so the two semantics carry over —
4958
+ * - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
4959
+ * stays out), and
4960
+ * - `requireTags`/`requireCards` inject from the full pre-filter pool,
4961
+ * *bypassing* gating (use when you want a card regardless of reachability).
4962
+ * Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
4963
+ * user has already seen — pass `unseenOnly: false` if that matters.
4964
+ *
4965
+ * Cost note: like {@link diagnoseCardSpace}, this scans every card through the
4966
+ * filters, so it's heavier than a normal replan. Intended for one-shot
4967
+ * out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
4968
+ *
4969
+ * @param opts.hints Optional ephemeral hints to apply after the filter chain.
4970
+ * @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
4971
+ * @param opts.threshold Min score to count as reachable (default 0.10).
4972
+ * @param opts.limit Optional cap on results (already sorted desc).
4973
+ */
4974
+ async forecast(opts) {
4975
+ const threshold = opts?.threshold ?? 0.1;
4976
+ const unseenOnly = opts?.unseenOnly ?? true;
4977
+ const courseId = this.course.getCourseID();
4978
+ const allCardIds = await this.course.getAllCardIds();
4979
+ let cards = allCardIds.map((cardId) => ({
4980
+ cardId,
4981
+ courseId,
4982
+ score: 1,
4983
+ provenance: []
4984
+ }));
4985
+ cards = await this.hydrateTags(cards);
4986
+ const fullPool = cards.slice();
4987
+ const context = await this.buildContext();
4988
+ for (const filter of this.filters) {
4989
+ cards = await filter.transform(cards, context);
4990
+ }
4991
+ if (opts?.hints) {
4992
+ cards = this.applyHints(cards, opts.hints, fullPool);
4993
+ }
4994
+ cards = cards.filter((c) => c.score >= threshold);
4995
+ if (unseenOnly) {
4996
+ let encountered;
4997
+ try {
4998
+ encountered = new Set(await this.user.getSeenCards(courseId));
4999
+ } catch {
5000
+ encountered = /* @__PURE__ */ new Set();
5001
+ }
5002
+ cards = cards.filter((c) => !encountered.has(c.cardId));
5003
+ }
5004
+ cards.sort((a, b) => b.score - a.score);
5005
+ return opts?.limit ? cards.slice(0, opts.limit) : cards;
5006
+ }
4856
5007
  /**
4857
5008
  * Scan every card in the course through the filter chain and report
4858
5009
  * how many are "well indicated" (score >= threshold) for the current user.
@@ -5117,6 +5268,7 @@ var init_3 = __esm({
5117
5268
  "./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
5118
5269
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
5119
5270
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
5271
+ "./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
5120
5272
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
5121
5273
  "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
5122
5274
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
@@ -5149,11 +5301,14 @@ __export(navigators_exports, {
5149
5301
  NavigatorRole: () => NavigatorRole,
5150
5302
  NavigatorRoles: () => NavigatorRoles,
5151
5303
  Navigators: () => Navigators,
5304
+ clearSrsBacklogDebug: () => clearSrsBacklogDebug,
5152
5305
  diversityRerank: () => diversityRerank,
5306
+ getActivePipeline: () => getActivePipeline,
5153
5307
  getCardOrigin: () => getCardOrigin,
5154
5308
  getRegisteredNavigator: () => getRegisteredNavigator,
5155
5309
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
5156
5310
  getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
5311
+ getSrsBacklogDebug: () => getSrsBacklogDebug,
5157
5312
  hasRegisteredNavigator: () => hasRegisteredNavigator,
5158
5313
  initializeNavigatorRegistry: () => initializeNavigatorRegistry,
5159
5314
  isFilter: () => isFilter,
@@ -5235,6 +5390,7 @@ var init_navigators = __esm({
5235
5390
  "use strict";
5236
5391
  init_diversityRerank();
5237
5392
  init_PipelineDebugger();
5393
+ init_SrsDebugger();
5238
5394
  init_logger();
5239
5395
  init_();
5240
5396
  init_2();
@@ -8253,6 +8409,7 @@ __export(core_exports, {
8253
8409
  aggregateOutcomesForGradient: () => aggregateOutcomesForGradient,
8254
8410
  areQuestionRecords: () => areQuestionRecords,
8255
8411
  buildStrategyStateId: () => buildStrategyStateId,
8412
+ clearSrsBacklogDebug: () => clearSrsBacklogDebug,
8256
8413
  computeDeviation: () => computeDeviation,
8257
8414
  computeEffectiveWeight: () => computeEffectiveWeight,
8258
8415
  computeOutcomeSignal: () => computeOutcomeSignal,
@@ -8261,12 +8418,14 @@ __export(core_exports, {
8261
8418
  createOrchestrationContext: () => createOrchestrationContext,
8262
8419
  diversityRerank: () => diversityRerank,
8263
8420
  docIsDeleted: () => docIsDeleted,
8421
+ getActivePipeline: () => getActivePipeline,
8264
8422
  getCardHistoryID: () => getCardHistoryID,
8265
8423
  getCardOrigin: () => getCardOrigin,
8266
8424
  getDefaultLearnableWeight: () => getDefaultLearnableWeight,
8267
8425
  getRegisteredNavigator: () => getRegisteredNavigator,
8268
8426
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
8269
8427
  getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
8428
+ getSrsBacklogDebug: () => getSrsBacklogDebug,
8270
8429
  getStudySource: () => getStudySource,
8271
8430
  hasRegisteredNavigator: () => hasRegisteredNavigator,
8272
8431
  importParsedCards: () => importParsedCards,
@@ -8321,6 +8480,7 @@ init_core();
8321
8480
  aggregateOutcomesForGradient,
8322
8481
  areQuestionRecords,
8323
8482
  buildStrategyStateId,
8483
+ clearSrsBacklogDebug,
8324
8484
  computeDeviation,
8325
8485
  computeEffectiveWeight,
8326
8486
  computeOutcomeSignal,
@@ -8329,12 +8489,14 @@ init_core();
8329
8489
  createOrchestrationContext,
8330
8490
  diversityRerank,
8331
8491
  docIsDeleted,
8492
+ getActivePipeline,
8332
8493
  getCardHistoryID,
8333
8494
  getCardOrigin,
8334
8495
  getDefaultLearnableWeight,
8335
8496
  getRegisteredNavigator,
8336
8497
  getRegisteredNavigatorNames,
8337
8498
  getRegisteredNavigatorRole,
8499
+ getSrsBacklogDebug,
8338
8500
  getStudySource,
8339
8501
  hasRegisteredNavigator,
8340
8502
  importParsedCards,