@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
@@ -791,6 +791,7 @@ __export(PipelineDebugger_exports, {
791
791
  buildRunReport: () => buildRunReport,
792
792
  captureRun: () => captureRun,
793
793
  clearRunHistory: () => clearRunHistory,
794
+ getActivePipeline: () => getActivePipeline,
794
795
  mountPipelineDebugger: () => mountPipelineDebugger,
795
796
  pipelineDebugAPI: () => pipelineDebugAPI,
796
797
  registerPipelineForDebug: () => registerPipelineForDebug
@@ -798,6 +799,9 @@ __export(PipelineDebugger_exports, {
798
799
  function registerPipelineForDebug(pipeline) {
799
800
  _activePipeline = pipeline;
800
801
  }
802
+ function getActivePipeline() {
803
+ return _activePipeline;
804
+ }
801
805
  function clearRunHistory() {
802
806
  runHistory.length = 0;
803
807
  }
@@ -1613,6 +1617,30 @@ Example:
1613
1617
  }
1614
1618
  });
1615
1619
 
1620
+ // src/core/navigators/SrsDebugger.ts
1621
+ var SrsDebugger_exports = {};
1622
+ __export(SrsDebugger_exports, {
1623
+ captureSrsBacklog: () => captureSrsBacklog,
1624
+ clearSrsBacklogDebug: () => clearSrsBacklogDebug,
1625
+ getSrsBacklogDebug: () => getSrsBacklogDebug
1626
+ });
1627
+ function captureSrsBacklog(snapshot) {
1628
+ snapshots.set(snapshot.courseId, snapshot);
1629
+ }
1630
+ function getSrsBacklogDebug() {
1631
+ return [...snapshots.values()].sort((a, b) => b.timestamp - a.timestamp);
1632
+ }
1633
+ function clearSrsBacklogDebug() {
1634
+ snapshots.clear();
1635
+ }
1636
+ var snapshots;
1637
+ var init_SrsDebugger = __esm({
1638
+ "src/core/navigators/SrsDebugger.ts"() {
1639
+ "use strict";
1640
+ snapshots = /* @__PURE__ */ new Map();
1641
+ }
1642
+ });
1643
+
1616
1644
  // src/core/navigators/generators/CompositeGenerator.ts
1617
1645
  var CompositeGenerator_exports = {};
1618
1646
  __export(CompositeGenerator_exports, {
@@ -1980,7 +2008,7 @@ function shuffleInPlace(arr) {
1980
2008
  function pickTopByScore(cards, limit) {
1981
2009
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1982
2010
  }
1983
- 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;
2011
+ 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;
1984
2012
  var init_prescribed = __esm({
1985
2013
  "src/core/navigators/generators/prescribed.ts"() {
1986
2014
  "use strict";
@@ -1997,6 +2025,9 @@ var init_prescribed = __esm({
1997
2025
  BASE_SUPPORT_SCORE = 0.8;
1998
2026
  DISCOVERED_SUPPORT_SCORE = 12;
1999
2027
  BASE_PRACTICE_SCORE = 1;
2028
+ PRACTICE_BASE_MULT = 2;
2029
+ MAX_PRACTICE_MULTIPLIER = 4;
2030
+ PRACTICE_STALENESS_BUMP_PER_DAY = 0.5;
2000
2031
  MAX_TARGET_MULTIPLIER = 8;
2001
2032
  MAX_SUPPORT_MULTIPLIER = 4;
2002
2033
  PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
@@ -2056,6 +2087,8 @@ var init_prescribed = __esm({
2056
2087
  const emitted = [];
2057
2088
  const emittedIds = /* @__PURE__ */ new Set();
2058
2089
  const groupRuntimes = [];
2090
+ const priorPracticeDebt = progress.practiceDebt ?? {};
2091
+ const nextPracticeDebt = {};
2059
2092
  for (const group of this.config.groups) {
2060
2093
  const runtime = this.buildGroupRuntimeState({
2061
2094
  group,
@@ -2113,10 +2146,13 @@ var init_prescribed = __esm({
2113
2146
  userTagElo,
2114
2147
  userGlobalElo,
2115
2148
  activeIds,
2116
- seenIds
2149
+ seenIds,
2150
+ priorPracticeDebt,
2151
+ nextPracticeDebt
2117
2152
  });
2118
2153
  emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
2119
2154
  }
2155
+ nextState.practiceDebt = nextPracticeDebt;
2120
2156
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
2121
2157
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
2122
2158
  boostTags: hintSummary.boostTags,
@@ -2450,9 +2486,16 @@ var init_prescribed = __esm({
2450
2486
  * `practiceMinCount`), this resolves cards carrying that tag and emits them
2451
2487
  * into the candidate pool. It exists because global-ELO retrieval
2452
2488
  * systematically fails to fetch the (low-ELO) drill cards for a
2453
- * freshly-introduced skill — putting them in the pool here lets the pipeline's
2454
- * scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
2455
- * this method's job; it only guarantees presence.
2489
+ * freshly-introduced skill — putting them in the pool here guarantees presence.
2490
+ *
2491
+ * Emphasis is a **practice-debt pressure** (parallel to SRS backlog pressure):
2492
+ * cards score `base × multiplier`, where the multiplier starts at
2493
+ * PRACTICE_BASE_MULT (so a few reps land promptly post-intro, competing with
2494
+ * pressured reviews) and escalates by how long the debt has stayed open
2495
+ * (per-tag, time-based via `priorPracticeDebt`/`nextPracticeDebt`), clamped at
2496
+ * MAX_PRACTICE_MULTIPLIER. The debt is durable and self-discharges the instant
2497
+ * the skill reaches `practiceMinCount` — so this no longer relies on the
2498
+ * session-scoped intro boost to actually surface.
2456
2499
  *
2457
2500
  * Fully data-driven: the unlock relation comes from the hierarchy config and
2458
2501
  * practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
@@ -2467,7 +2510,9 @@ var init_prescribed = __esm({
2467
2510
  userTagElo,
2468
2511
  userGlobalElo,
2469
2512
  activeIds,
2470
- seenIds
2513
+ seenIds,
2514
+ priorPracticeDebt,
2515
+ nextPracticeDebt
2471
2516
  } = args;
2472
2517
  const patterns = group.practiceTagPatterns ?? [];
2473
2518
  if (patterns.length === 0) return [];
@@ -2477,6 +2522,20 @@ var init_prescribed = __esm({
2477
2522
  (tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
2478
2523
  );
2479
2524
  if (practiceTags.length === 0) return [];
2525
+ const now = Date.now();
2526
+ const DAY_MS = 24 * 60 * 60 * 1e3;
2527
+ const tagMultiplier = /* @__PURE__ */ new Map();
2528
+ for (const tag of practiceTags) {
2529
+ const firstOwedAt = priorPracticeDebt[tag] ?? isoNow();
2530
+ nextPracticeDebt[tag] = firstOwedAt;
2531
+ const staleDays = Math.max(0, (now - new Date(firstOwedAt).getTime()) / DAY_MS);
2532
+ const mult = clamp(
2533
+ PRACTICE_BASE_MULT + staleDays * PRACTICE_STALENESS_BUMP_PER_DAY,
2534
+ PRACTICE_BASE_MULT,
2535
+ MAX_PRACTICE_MULTIPLIER
2536
+ );
2537
+ tagMultiplier.set(tag, mult);
2538
+ }
2480
2539
  const practiceCardIds = this.findDiscoveredSupportCards({
2481
2540
  supportTags: practiceTags,
2482
2541
  cardsByTag,
@@ -2492,18 +2551,25 @@ var init_prescribed = __esm({
2492
2551
  const cards = [];
2493
2552
  for (const cardId of practiceCardIds) {
2494
2553
  emittedIds.add(cardId);
2554
+ let mult = PRACTICE_BASE_MULT;
2555
+ for (const tag of practiceTags) {
2556
+ if (cardsByTag.get(tag)?.includes(cardId) ?? false) {
2557
+ mult = Math.max(mult, tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT);
2558
+ }
2559
+ }
2560
+ const score = BASE_PRACTICE_SCORE * mult;
2495
2561
  cards.push({
2496
2562
  cardId,
2497
2563
  courseId,
2498
- score: BASE_PRACTICE_SCORE,
2564
+ score,
2499
2565
  provenance: [
2500
2566
  {
2501
2567
  strategy: "prescribed",
2502
2568
  strategyName: this.strategyName || this.name,
2503
2569
  strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2504
2570
  action: "generated",
2505
- score: BASE_PRACTICE_SCORE,
2506
- reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
2571
+ score,
2572
+ 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}`
2507
2573
  }
2508
2574
  ]
2509
2575
  });
@@ -2677,14 +2743,15 @@ __export(srs_exports, {
2677
2743
  default: () => SRSNavigator
2678
2744
  });
2679
2745
  import moment3 from "moment";
2680
- var DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_PRESSURE, SRSNavigator;
2746
+ var DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_MULTIPLIER, SRSNavigator;
2681
2747
  var init_srs = __esm({
2682
2748
  "src/core/navigators/generators/srs.ts"() {
2683
2749
  "use strict";
2684
2750
  init_navigators();
2751
+ init_SrsDebugger();
2685
2752
  init_logger();
2686
2753
  DEFAULT_HEALTHY_BACKLOG = 20;
2687
- MAX_BACKLOG_PRESSURE = 0.5;
2754
+ MAX_BACKLOG_MULTIPLIER = 2;
2688
2755
  SRSNavigator = class extends ContentNavigator {
2689
2756
  /** Human-readable name for CardGenerator interface */
2690
2757
  name;
@@ -2751,9 +2818,18 @@ var init_srs = __esm({
2751
2818
  }
2752
2819
  }
2753
2820
  }
2754
- const backlogPressure = this.computeBacklogPressure(dueReviews.length);
2821
+ const backlogMultiplier = this.computeBacklogMultiplier(dueReviews.length);
2822
+ const notDue = reviews.filter((r) => !now.isAfter(moment3.utc(r.reviewTime)));
2823
+ let nextDueIn = null;
2824
+ if (notDue.length > 0) {
2825
+ const next = notDue.reduce(
2826
+ (a, b) => moment3.utc(a.reviewTime).isBefore(moment3.utc(b.reviewTime)) ? a : b
2827
+ );
2828
+ const until = moment3.duration(moment3.utc(next.reviewTime).diff(now));
2829
+ nextDueIn = until.asHours() < 1 ? `${Math.round(until.asMinutes())}m` : until.asHours() < 24 ? `${Math.round(until.asHours())}h` : `${Math.round(until.asDays())}d`;
2830
+ }
2755
2831
  if (dueReviews.length > 0) {
2756
- const pressureNote = backlogPressure > 0 ? ` [backlog pressure: +${backlogPressure.toFixed(2)}]` : ` [healthy backlog]`;
2832
+ const pressureNote = backlogMultiplier > 1 ? ` [backlog pressure: \xD7${backlogMultiplier.toFixed(2)}]` : ` [healthy backlog]`;
2757
2833
  logger.info(
2758
2834
  `[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
2759
2835
  );
@@ -2772,7 +2848,7 @@ var init_srs = __esm({
2772
2848
  logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
2773
2849
  }
2774
2850
  const scored = dueReviews.map((review) => {
2775
- const { score, reason } = this.computeUrgencyScore(review, now, backlogPressure);
2851
+ const { score, reason } = this.computeUrgencyScore(review, now, backlogMultiplier);
2776
2852
  return {
2777
2853
  cardId: review.cardId,
2778
2854
  courseId: review.courseId,
@@ -2790,30 +2866,42 @@ var init_srs = __esm({
2790
2866
  ]
2791
2867
  };
2792
2868
  });
2793
- return { cards: scored.sort((a, b) => b.score - a.score).slice(0, limit) };
2869
+ const sorted = scored.sort((a, b) => b.score - a.score);
2870
+ captureSrsBacklog({
2871
+ courseId,
2872
+ scheduledTotal: reviews.length,
2873
+ dueNow: dueReviews.length,
2874
+ healthyBacklog: this.healthyBacklog,
2875
+ backlogMultiplier,
2876
+ maxBacklogMultiplier: MAX_BACKLOG_MULTIPLIER,
2877
+ topReviewScore: sorted.length > 0 ? sorted[0].score : null,
2878
+ nextDueIn,
2879
+ timestamp: Date.now()
2880
+ });
2881
+ return { cards: sorted.slice(0, limit) };
2794
2882
  }
2795
2883
  /**
2796
- * Compute backlog pressure based on number of due reviews.
2884
+ * Compute the multiplicative backlog pressure based on number of due reviews.
2797
2885
  *
2798
- * Backlog pressure is 0 when at or below healthy threshold,
2799
- * and increases linearly above it, maxing out at MAX_BACKLOG_PRESSURE.
2886
+ * ×1.0 at or below the healthy threshold (no boost), increasing linearly above
2887
+ * it and maxing out at MAX_BACKLOG_MULTIPLIER at the healthy backlog.
2800
2888
  *
2801
- * Examples (with default healthyBacklog=20):
2802
- * - 10 due reviews → 0.00 (healthy)
2803
- * - 20 due reviews → 0.00 (at threshold)
2804
- * - 40 due reviews → 0.25 (2x threshold)
2805
- * - 60 due reviews → 0.50 (3x threshold, maxed)
2889
+ * Examples (with default healthyBacklog=20, MAX_BACKLOG_MULTIPLIER=2.0):
2890
+ * - 10 due reviews → ×1.00 (healthy)
2891
+ * - 20 due reviews → ×1.00 (at threshold)
2892
+ * - 40 due reviews → ×1.50 (2x threshold)
2893
+ * - 60 due reviews → ×2.00 (3x threshold, maxed)
2806
2894
  *
2807
2895
  * @param dueCount - Number of reviews currently due
2808
- * @returns Backlog pressure score to add to urgency (0 to MAX_BACKLOG_PRESSURE)
2896
+ * @returns Multiplier applied to review urgency (1.0 to MAX_BACKLOG_MULTIPLIER)
2809
2897
  */
2810
- computeBacklogPressure(dueCount) {
2898
+ computeBacklogMultiplier(dueCount) {
2811
2899
  if (dueCount <= this.healthyBacklog) {
2812
- return 0;
2900
+ return 1;
2813
2901
  }
2814
2902
  const excess = dueCount - this.healthyBacklog;
2815
- const pressure = excess / this.healthyBacklog * (MAX_BACKLOG_PRESSURE / 2);
2816
- return Math.min(MAX_BACKLOG_PRESSURE, pressure);
2903
+ const multiplier = 1 + excess / this.healthyBacklog * ((MAX_BACKLOG_MULTIPLIER - 1) / 2);
2904
+ return Math.min(MAX_BACKLOG_MULTIPLIER, multiplier);
2817
2905
  }
2818
2906
  /**
2819
2907
  * Compute urgency score for a review card.
@@ -2828,19 +2916,20 @@ var init_srs = __esm({
2828
2916
  * - 30 days (720h) → ~0.56
2829
2917
  * - 180 days → ~0.30
2830
2918
  *
2831
- * 3. Backlog pressure = global boost when review backlog exceeds healthy threshold
2832
- * - At healthy backlog: 0
2833
- * - At 2x healthy: +0.25
2834
- * - At 3x+ healthy: +0.50 (max)
2919
+ * 3. Backlog pressure = global *multiplier* when review backlog exceeds the
2920
+ * healthy threshold (×1.0 healthy up to MAX_BACKLOG_MULTIPLIER at 3×).
2835
2921
  *
2836
- * Combined: base 0.5 + (urgency factors * 0.45) + backlog pressure
2837
- * Result range: 0.5 to 1.0 (uncapped to allow high-urgency reviews to compete with new cards)
2922
+ * Combined: (base 0.5 + urgency factors * 0.45) × backlog multiplier.
2923
+ * Per-card range before pressure: ~0.57–0.95. NOT clamped to 1.0 under a
2924
+ * heavy backlog reviews scale onto the open scale to compete with (and exceed)
2925
+ * new cards; what keeps them from running away is the bounded multiplier, not
2926
+ * a hard ceiling.
2838
2927
  *
2839
2928
  * @param review - The scheduled card to score
2840
2929
  * @param now - Current time
2841
- * @param backlogPressure - Pre-computed backlog pressure (0 to 0.5)
2930
+ * @param backlogMultiplier - Pre-computed backlog multiplier (1.0 to MAX_BACKLOG_MULTIPLIER)
2842
2931
  */
2843
- computeUrgencyScore(review, now, backlogPressure) {
2932
+ computeUrgencyScore(review, now, backlogMultiplier) {
2844
2933
  const scheduledAt = moment3.utc(review.scheduledAt);
2845
2934
  const due = moment3.utc(review.reviewTime);
2846
2935
  const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
@@ -2850,15 +2939,15 @@ var init_srs = __esm({
2850
2939
  const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
2851
2940
  const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
2852
2941
  const baseScore = 0.5 + urgency * 0.45;
2853
- const score = Math.min(1, baseScore + backlogPressure);
2942
+ const score = baseScore * backlogMultiplier;
2854
2943
  const reasonParts = [
2855
2944
  `${Math.round(hoursOverdue)}h overdue`,
2856
2945
  `interval: ${Math.round(intervalHours)}h`,
2857
2946
  `relative: ${relativeOverdue.toFixed(2)}`,
2858
2947
  `recency: ${recencyFactor.toFixed(2)}`
2859
2948
  ];
2860
- if (backlogPressure > 0) {
2861
- reasonParts.push(`backlog: +${backlogPressure.toFixed(2)}`);
2949
+ if (backlogMultiplier > 1) {
2950
+ reasonParts.push(`backlog: \xD7${backlogMultiplier.toFixed(2)}`);
2862
2951
  }
2863
2952
  reasonParts.push("review");
2864
2953
  const reason = reasonParts.join(", ");
@@ -4830,6 +4919,68 @@ var init_Pipeline = __esm({
4830
4919
  // ---------------------------------------------------------------------------
4831
4920
  // Card-space diagnostic
4832
4921
  // ---------------------------------------------------------------------------
4922
+ /**
4923
+ * Commit-free forecast: score the user's full card space through the filter
4924
+ * chain and return the cards that are currently *reachable* (score >=
4925
+ * threshold), optionally nudged by caller-supplied hints and/or restricted
4926
+ * to cards the user hasn't seen yet.
4927
+ *
4928
+ * This is a GENERIC primitive — it returns scored, tag-hydrated cards and
4929
+ * stops there. It has no knowledge of any particular tag convention; callers
4930
+ * decide what the surviving cards mean (e.g. filter to their own "intro"
4931
+ * tag family). Nothing is written and no session is started.
4932
+ *
4933
+ * The optional `hints` are the "out-of-band kick": they run through the same
4934
+ * {@link applyHints} path a live replan uses, so the two semantics carry over —
4935
+ * - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
4936
+ * stays out), and
4937
+ * - `requireTags`/`requireCards` inject from the full pre-filter pool,
4938
+ * *bypassing* gating (use when you want a card regardless of reachability).
4939
+ * Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
4940
+ * user has already seen — pass `unseenOnly: false` if that matters.
4941
+ *
4942
+ * Cost note: like {@link diagnoseCardSpace}, this scans every card through the
4943
+ * filters, so it's heavier than a normal replan. Intended for one-shot
4944
+ * out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
4945
+ *
4946
+ * @param opts.hints Optional ephemeral hints to apply after the filter chain.
4947
+ * @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
4948
+ * @param opts.threshold Min score to count as reachable (default 0.10).
4949
+ * @param opts.limit Optional cap on results (already sorted desc).
4950
+ */
4951
+ async forecast(opts) {
4952
+ const threshold = opts?.threshold ?? 0.1;
4953
+ const unseenOnly = opts?.unseenOnly ?? true;
4954
+ const courseId = this.course.getCourseID();
4955
+ const allCardIds = await this.course.getAllCardIds();
4956
+ let cards = allCardIds.map((cardId) => ({
4957
+ cardId,
4958
+ courseId,
4959
+ score: 1,
4960
+ provenance: []
4961
+ }));
4962
+ cards = await this.hydrateTags(cards);
4963
+ const fullPool = cards.slice();
4964
+ const context = await this.buildContext();
4965
+ for (const filter of this.filters) {
4966
+ cards = await filter.transform(cards, context);
4967
+ }
4968
+ if (opts?.hints) {
4969
+ cards = this.applyHints(cards, opts.hints, fullPool);
4970
+ }
4971
+ cards = cards.filter((c) => c.score >= threshold);
4972
+ if (unseenOnly) {
4973
+ let encountered;
4974
+ try {
4975
+ encountered = new Set(await this.user.getSeenCards(courseId));
4976
+ } catch {
4977
+ encountered = /* @__PURE__ */ new Set();
4978
+ }
4979
+ cards = cards.filter((c) => !encountered.has(c.cardId));
4980
+ }
4981
+ cards.sort((a, b) => b.score - a.score);
4982
+ return opts?.limit ? cards.slice(0, opts.limit) : cards;
4983
+ }
4833
4984
  /**
4834
4985
  * Scan every card in the course through the filter chain and report
4835
4986
  * how many are "well indicated" (score >= threshold) for the current user.
@@ -5094,6 +5245,7 @@ var init_3 = __esm({
5094
5245
  "./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
5095
5246
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
5096
5247
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
5248
+ "./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
5097
5249
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
5098
5250
  "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
5099
5251
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
@@ -5126,11 +5278,14 @@ __export(navigators_exports, {
5126
5278
  NavigatorRole: () => NavigatorRole,
5127
5279
  NavigatorRoles: () => NavigatorRoles,
5128
5280
  Navigators: () => Navigators,
5281
+ clearSrsBacklogDebug: () => clearSrsBacklogDebug,
5129
5282
  diversityRerank: () => diversityRerank,
5283
+ getActivePipeline: () => getActivePipeline,
5130
5284
  getCardOrigin: () => getCardOrigin,
5131
5285
  getRegisteredNavigator: () => getRegisteredNavigator,
5132
5286
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
5133
5287
  getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
5288
+ getSrsBacklogDebug: () => getSrsBacklogDebug,
5134
5289
  hasRegisteredNavigator: () => hasRegisteredNavigator,
5135
5290
  initializeNavigatorRegistry: () => initializeNavigatorRegistry,
5136
5291
  isFilter: () => isFilter,
@@ -5212,6 +5367,7 @@ var init_navigators = __esm({
5212
5367
  "use strict";
5213
5368
  init_diversityRerank();
5214
5369
  init_PipelineDebugger();
5370
+ init_SrsDebugger();
5215
5371
  init_logger();
5216
5372
  init_();
5217
5373
  init_2();
@@ -8246,6 +8402,7 @@ export {
8246
8402
  aggregateOutcomesForGradient,
8247
8403
  areQuestionRecords,
8248
8404
  buildStrategyStateId,
8405
+ clearSrsBacklogDebug,
8249
8406
  computeDeviation,
8250
8407
  computeEffectiveWeight,
8251
8408
  computeOutcomeSignal,
@@ -8254,12 +8411,14 @@ export {
8254
8411
  createOrchestrationContext,
8255
8412
  diversityRerank,
8256
8413
  docIsDeleted,
8414
+ getActivePipeline,
8257
8415
  getCardHistoryID,
8258
8416
  getCardOrigin,
8259
8417
  getDefaultLearnableWeight,
8260
8418
  getRegisteredNavigator,
8261
8419
  getRegisteredNavigatorNames,
8262
8420
  getRegisteredNavigatorRole,
8421
+ getSrsBacklogDebug,
8263
8422
  getStudySource,
8264
8423
  hasRegisteredNavigator,
8265
8424
  importParsedCards,