@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
@@ -716,6 +716,7 @@ __export(PipelineDebugger_exports, {
716
716
  buildRunReport: () => buildRunReport,
717
717
  captureRun: () => captureRun,
718
718
  clearRunHistory: () => clearRunHistory,
719
+ getActivePipeline: () => getActivePipeline,
719
720
  mountPipelineDebugger: () => mountPipelineDebugger,
720
721
  pipelineDebugAPI: () => pipelineDebugAPI,
721
722
  registerPipelineForDebug: () => registerPipelineForDebug
@@ -723,6 +724,9 @@ __export(PipelineDebugger_exports, {
723
724
  function registerPipelineForDebug(pipeline) {
724
725
  _activePipeline = pipeline;
725
726
  }
727
+ function getActivePipeline() {
728
+ return _activePipeline;
729
+ }
726
730
  function clearRunHistory() {
727
731
  runHistory.length = 0;
728
732
  }
@@ -1538,6 +1542,30 @@ Example:
1538
1542
  }
1539
1543
  });
1540
1544
 
1545
+ // src/core/navigators/SrsDebugger.ts
1546
+ var SrsDebugger_exports = {};
1547
+ __export(SrsDebugger_exports, {
1548
+ captureSrsBacklog: () => captureSrsBacklog,
1549
+ clearSrsBacklogDebug: () => clearSrsBacklogDebug,
1550
+ getSrsBacklogDebug: () => getSrsBacklogDebug
1551
+ });
1552
+ function captureSrsBacklog(snapshot) {
1553
+ snapshots.set(snapshot.courseId, snapshot);
1554
+ }
1555
+ function getSrsBacklogDebug() {
1556
+ return [...snapshots.values()].sort((a, b) => b.timestamp - a.timestamp);
1557
+ }
1558
+ function clearSrsBacklogDebug() {
1559
+ snapshots.clear();
1560
+ }
1561
+ var snapshots;
1562
+ var init_SrsDebugger = __esm({
1563
+ "src/core/navigators/SrsDebugger.ts"() {
1564
+ "use strict";
1565
+ snapshots = /* @__PURE__ */ new Map();
1566
+ }
1567
+ });
1568
+
1541
1569
  // src/core/navigators/generators/CompositeGenerator.ts
1542
1570
  var CompositeGenerator_exports = {};
1543
1571
  __export(CompositeGenerator_exports, {
@@ -1905,7 +1933,7 @@ function shuffleInPlace(arr) {
1905
1933
  function pickTopByScore(cards, limit) {
1906
1934
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1907
1935
  }
1908
- 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;
1936
+ 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;
1909
1937
  var init_prescribed = __esm({
1910
1938
  "src/core/navigators/generators/prescribed.ts"() {
1911
1939
  "use strict";
@@ -1922,6 +1950,9 @@ var init_prescribed = __esm({
1922
1950
  BASE_SUPPORT_SCORE = 0.8;
1923
1951
  DISCOVERED_SUPPORT_SCORE = 12;
1924
1952
  BASE_PRACTICE_SCORE = 1;
1953
+ PRACTICE_BASE_MULT = 2;
1954
+ MAX_PRACTICE_MULTIPLIER = 4;
1955
+ PRACTICE_STALENESS_BUMP_PER_DAY = 0.5;
1925
1956
  MAX_TARGET_MULTIPLIER = 8;
1926
1957
  MAX_SUPPORT_MULTIPLIER = 4;
1927
1958
  PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
@@ -1981,6 +2012,8 @@ var init_prescribed = __esm({
1981
2012
  const emitted = [];
1982
2013
  const emittedIds = /* @__PURE__ */ new Set();
1983
2014
  const groupRuntimes = [];
2015
+ const priorPracticeDebt = progress.practiceDebt ?? {};
2016
+ const nextPracticeDebt = {};
1984
2017
  for (const group of this.config.groups) {
1985
2018
  const runtime = this.buildGroupRuntimeState({
1986
2019
  group,
@@ -2038,10 +2071,13 @@ var init_prescribed = __esm({
2038
2071
  userTagElo,
2039
2072
  userGlobalElo,
2040
2073
  activeIds,
2041
- seenIds
2074
+ seenIds,
2075
+ priorPracticeDebt,
2076
+ nextPracticeDebt
2042
2077
  });
2043
2078
  emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
2044
2079
  }
2080
+ nextState.practiceDebt = nextPracticeDebt;
2045
2081
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
2046
2082
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
2047
2083
  boostTags: hintSummary.boostTags,
@@ -2375,9 +2411,16 @@ var init_prescribed = __esm({
2375
2411
  * `practiceMinCount`), this resolves cards carrying that tag and emits them
2376
2412
  * into the candidate pool. It exists because global-ELO retrieval
2377
2413
  * systematically fails to fetch the (low-ELO) drill cards for a
2378
- * freshly-introduced skill — putting them in the pool here lets the pipeline's
2379
- * scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
2380
- * this method's job; it only guarantees presence.
2414
+ * freshly-introduced skill — putting them in the pool here guarantees presence.
2415
+ *
2416
+ * Emphasis is a **practice-debt pressure** (parallel to SRS backlog pressure):
2417
+ * cards score `base × multiplier`, where the multiplier starts at
2418
+ * PRACTICE_BASE_MULT (so a few reps land promptly post-intro, competing with
2419
+ * pressured reviews) and escalates by how long the debt has stayed open
2420
+ * (per-tag, time-based via `priorPracticeDebt`/`nextPracticeDebt`), clamped at
2421
+ * MAX_PRACTICE_MULTIPLIER. The debt is durable and self-discharges the instant
2422
+ * the skill reaches `practiceMinCount` — so this no longer relies on the
2423
+ * session-scoped intro boost to actually surface.
2381
2424
  *
2382
2425
  * Fully data-driven: the unlock relation comes from the hierarchy config and
2383
2426
  * practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
@@ -2392,7 +2435,9 @@ var init_prescribed = __esm({
2392
2435
  userTagElo,
2393
2436
  userGlobalElo,
2394
2437
  activeIds,
2395
- seenIds
2438
+ seenIds,
2439
+ priorPracticeDebt,
2440
+ nextPracticeDebt
2396
2441
  } = args;
2397
2442
  const patterns = group.practiceTagPatterns ?? [];
2398
2443
  if (patterns.length === 0) return [];
@@ -2402,6 +2447,20 @@ var init_prescribed = __esm({
2402
2447
  (tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
2403
2448
  );
2404
2449
  if (practiceTags.length === 0) return [];
2450
+ const now = Date.now();
2451
+ const DAY_MS = 24 * 60 * 60 * 1e3;
2452
+ const tagMultiplier = /* @__PURE__ */ new Map();
2453
+ for (const tag of practiceTags) {
2454
+ const firstOwedAt = priorPracticeDebt[tag] ?? isoNow();
2455
+ nextPracticeDebt[tag] = firstOwedAt;
2456
+ const staleDays = Math.max(0, (now - new Date(firstOwedAt).getTime()) / DAY_MS);
2457
+ const mult = clamp(
2458
+ PRACTICE_BASE_MULT + staleDays * PRACTICE_STALENESS_BUMP_PER_DAY,
2459
+ PRACTICE_BASE_MULT,
2460
+ MAX_PRACTICE_MULTIPLIER
2461
+ );
2462
+ tagMultiplier.set(tag, mult);
2463
+ }
2405
2464
  const practiceCardIds = this.findDiscoveredSupportCards({
2406
2465
  supportTags: practiceTags,
2407
2466
  cardsByTag,
@@ -2417,18 +2476,25 @@ var init_prescribed = __esm({
2417
2476
  const cards = [];
2418
2477
  for (const cardId of practiceCardIds) {
2419
2478
  emittedIds.add(cardId);
2479
+ let mult = PRACTICE_BASE_MULT;
2480
+ for (const tag of practiceTags) {
2481
+ if (cardsByTag.get(tag)?.includes(cardId) ?? false) {
2482
+ mult = Math.max(mult, tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT);
2483
+ }
2484
+ }
2485
+ const score = BASE_PRACTICE_SCORE * mult;
2420
2486
  cards.push({
2421
2487
  cardId,
2422
2488
  courseId,
2423
- score: BASE_PRACTICE_SCORE,
2489
+ score,
2424
2490
  provenance: [
2425
2491
  {
2426
2492
  strategy: "prescribed",
2427
2493
  strategyName: this.strategyName || this.name,
2428
2494
  strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2429
2495
  action: "generated",
2430
- score: BASE_PRACTICE_SCORE,
2431
- reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
2496
+ score,
2497
+ 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}`
2432
2498
  }
2433
2499
  ]
2434
2500
  });
@@ -2602,14 +2668,15 @@ __export(srs_exports, {
2602
2668
  default: () => SRSNavigator
2603
2669
  });
2604
2670
  import moment from "moment";
2605
- var DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_PRESSURE, SRSNavigator;
2671
+ var DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_MULTIPLIER, SRSNavigator;
2606
2672
  var init_srs = __esm({
2607
2673
  "src/core/navigators/generators/srs.ts"() {
2608
2674
  "use strict";
2609
2675
  init_navigators();
2676
+ init_SrsDebugger();
2610
2677
  init_logger();
2611
2678
  DEFAULT_HEALTHY_BACKLOG = 20;
2612
- MAX_BACKLOG_PRESSURE = 0.5;
2679
+ MAX_BACKLOG_MULTIPLIER = 2;
2613
2680
  SRSNavigator = class extends ContentNavigator {
2614
2681
  /** Human-readable name for CardGenerator interface */
2615
2682
  name;
@@ -2676,9 +2743,18 @@ var init_srs = __esm({
2676
2743
  }
2677
2744
  }
2678
2745
  }
2679
- const backlogPressure = this.computeBacklogPressure(dueReviews.length);
2746
+ const backlogMultiplier = this.computeBacklogMultiplier(dueReviews.length);
2747
+ const notDue = reviews.filter((r) => !now.isAfter(moment.utc(r.reviewTime)));
2748
+ let nextDueIn = null;
2749
+ if (notDue.length > 0) {
2750
+ const next = notDue.reduce(
2751
+ (a, b) => moment.utc(a.reviewTime).isBefore(moment.utc(b.reviewTime)) ? a : b
2752
+ );
2753
+ const until = moment.duration(moment.utc(next.reviewTime).diff(now));
2754
+ nextDueIn = until.asHours() < 1 ? `${Math.round(until.asMinutes())}m` : until.asHours() < 24 ? `${Math.round(until.asHours())}h` : `${Math.round(until.asDays())}d`;
2755
+ }
2680
2756
  if (dueReviews.length > 0) {
2681
- const pressureNote = backlogPressure > 0 ? ` [backlog pressure: +${backlogPressure.toFixed(2)}]` : ` [healthy backlog]`;
2757
+ const pressureNote = backlogMultiplier > 1 ? ` [backlog pressure: \xD7${backlogMultiplier.toFixed(2)}]` : ` [healthy backlog]`;
2682
2758
  logger.info(
2683
2759
  `[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
2684
2760
  );
@@ -2697,7 +2773,7 @@ var init_srs = __esm({
2697
2773
  logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
2698
2774
  }
2699
2775
  const scored = dueReviews.map((review) => {
2700
- const { score, reason } = this.computeUrgencyScore(review, now, backlogPressure);
2776
+ const { score, reason } = this.computeUrgencyScore(review, now, backlogMultiplier);
2701
2777
  return {
2702
2778
  cardId: review.cardId,
2703
2779
  courseId: review.courseId,
@@ -2715,30 +2791,42 @@ var init_srs = __esm({
2715
2791
  ]
2716
2792
  };
2717
2793
  });
2718
- return { cards: scored.sort((a, b) => b.score - a.score).slice(0, limit) };
2794
+ const sorted = scored.sort((a, b) => b.score - a.score);
2795
+ captureSrsBacklog({
2796
+ courseId,
2797
+ scheduledTotal: reviews.length,
2798
+ dueNow: dueReviews.length,
2799
+ healthyBacklog: this.healthyBacklog,
2800
+ backlogMultiplier,
2801
+ maxBacklogMultiplier: MAX_BACKLOG_MULTIPLIER,
2802
+ topReviewScore: sorted.length > 0 ? sorted[0].score : null,
2803
+ nextDueIn,
2804
+ timestamp: Date.now()
2805
+ });
2806
+ return { cards: sorted.slice(0, limit) };
2719
2807
  }
2720
2808
  /**
2721
- * Compute backlog pressure based on number of due reviews.
2809
+ * Compute the multiplicative backlog pressure based on number of due reviews.
2722
2810
  *
2723
- * Backlog pressure is 0 when at or below healthy threshold,
2724
- * and increases linearly above it, maxing out at MAX_BACKLOG_PRESSURE.
2811
+ * ×1.0 at or below the healthy threshold (no boost), increasing linearly above
2812
+ * it and maxing out at MAX_BACKLOG_MULTIPLIER at the healthy backlog.
2725
2813
  *
2726
- * Examples (with default healthyBacklog=20):
2727
- * - 10 due reviews → 0.00 (healthy)
2728
- * - 20 due reviews → 0.00 (at threshold)
2729
- * - 40 due reviews → 0.25 (2x threshold)
2730
- * - 60 due reviews → 0.50 (3x threshold, maxed)
2814
+ * Examples (with default healthyBacklog=20, MAX_BACKLOG_MULTIPLIER=2.0):
2815
+ * - 10 due reviews → ×1.00 (healthy)
2816
+ * - 20 due reviews → ×1.00 (at threshold)
2817
+ * - 40 due reviews → ×1.50 (2x threshold)
2818
+ * - 60 due reviews → ×2.00 (3x threshold, maxed)
2731
2819
  *
2732
2820
  * @param dueCount - Number of reviews currently due
2733
- * @returns Backlog pressure score to add to urgency (0 to MAX_BACKLOG_PRESSURE)
2821
+ * @returns Multiplier applied to review urgency (1.0 to MAX_BACKLOG_MULTIPLIER)
2734
2822
  */
2735
- computeBacklogPressure(dueCount) {
2823
+ computeBacklogMultiplier(dueCount) {
2736
2824
  if (dueCount <= this.healthyBacklog) {
2737
- return 0;
2825
+ return 1;
2738
2826
  }
2739
2827
  const excess = dueCount - this.healthyBacklog;
2740
- const pressure = excess / this.healthyBacklog * (MAX_BACKLOG_PRESSURE / 2);
2741
- return Math.min(MAX_BACKLOG_PRESSURE, pressure);
2828
+ const multiplier = 1 + excess / this.healthyBacklog * ((MAX_BACKLOG_MULTIPLIER - 1) / 2);
2829
+ return Math.min(MAX_BACKLOG_MULTIPLIER, multiplier);
2742
2830
  }
2743
2831
  /**
2744
2832
  * Compute urgency score for a review card.
@@ -2753,19 +2841,20 @@ var init_srs = __esm({
2753
2841
  * - 30 days (720h) → ~0.56
2754
2842
  * - 180 days → ~0.30
2755
2843
  *
2756
- * 3. Backlog pressure = global boost when review backlog exceeds healthy threshold
2757
- * - At healthy backlog: 0
2758
- * - At 2x healthy: +0.25
2759
- * - At 3x+ healthy: +0.50 (max)
2844
+ * 3. Backlog pressure = global *multiplier* when review backlog exceeds the
2845
+ * healthy threshold (×1.0 healthy up to MAX_BACKLOG_MULTIPLIER at 3×).
2760
2846
  *
2761
- * Combined: base 0.5 + (urgency factors * 0.45) + backlog pressure
2762
- * Result range: 0.5 to 1.0 (uncapped to allow high-urgency reviews to compete with new cards)
2847
+ * Combined: (base 0.5 + urgency factors * 0.45) × backlog multiplier.
2848
+ * Per-card range before pressure: ~0.57–0.95. NOT clamped to 1.0 under a
2849
+ * heavy backlog reviews scale onto the open scale to compete with (and exceed)
2850
+ * new cards; what keeps them from running away is the bounded multiplier, not
2851
+ * a hard ceiling.
2763
2852
  *
2764
2853
  * @param review - The scheduled card to score
2765
2854
  * @param now - Current time
2766
- * @param backlogPressure - Pre-computed backlog pressure (0 to 0.5)
2855
+ * @param backlogMultiplier - Pre-computed backlog multiplier (1.0 to MAX_BACKLOG_MULTIPLIER)
2767
2856
  */
2768
- computeUrgencyScore(review, now, backlogPressure) {
2857
+ computeUrgencyScore(review, now, backlogMultiplier) {
2769
2858
  const scheduledAt = moment.utc(review.scheduledAt);
2770
2859
  const due = moment.utc(review.reviewTime);
2771
2860
  const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
@@ -2775,15 +2864,15 @@ var init_srs = __esm({
2775
2864
  const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
2776
2865
  const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
2777
2866
  const baseScore = 0.5 + urgency * 0.45;
2778
- const score = Math.min(1, baseScore + backlogPressure);
2867
+ const score = baseScore * backlogMultiplier;
2779
2868
  const reasonParts = [
2780
2869
  `${Math.round(hoursOverdue)}h overdue`,
2781
2870
  `interval: ${Math.round(intervalHours)}h`,
2782
2871
  `relative: ${relativeOverdue.toFixed(2)}`,
2783
2872
  `recency: ${recencyFactor.toFixed(2)}`
2784
2873
  ];
2785
- if (backlogPressure > 0) {
2786
- reasonParts.push(`backlog: +${backlogPressure.toFixed(2)}`);
2874
+ if (backlogMultiplier > 1) {
2875
+ reasonParts.push(`backlog: \xD7${backlogMultiplier.toFixed(2)}`);
2787
2876
  }
2788
2877
  reasonParts.push("review");
2789
2878
  const reason = reasonParts.join(", ");
@@ -4509,6 +4598,68 @@ var init_Pipeline = __esm({
4509
4598
  // ---------------------------------------------------------------------------
4510
4599
  // Card-space diagnostic
4511
4600
  // ---------------------------------------------------------------------------
4601
+ /**
4602
+ * Commit-free forecast: score the user's full card space through the filter
4603
+ * chain and return the cards that are currently *reachable* (score >=
4604
+ * threshold), optionally nudged by caller-supplied hints and/or restricted
4605
+ * to cards the user hasn't seen yet.
4606
+ *
4607
+ * This is a GENERIC primitive — it returns scored, tag-hydrated cards and
4608
+ * stops there. It has no knowledge of any particular tag convention; callers
4609
+ * decide what the surviving cards mean (e.g. filter to their own "intro"
4610
+ * tag family). Nothing is written and no session is started.
4611
+ *
4612
+ * The optional `hints` are the "out-of-band kick": they run through the same
4613
+ * {@link applyHints} path a live replan uses, so the two semantics carry over —
4614
+ * - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
4615
+ * stays out), and
4616
+ * - `requireTags`/`requireCards` inject from the full pre-filter pool,
4617
+ * *bypassing* gating (use when you want a card regardless of reachability).
4618
+ * Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
4619
+ * user has already seen — pass `unseenOnly: false` if that matters.
4620
+ *
4621
+ * Cost note: like {@link diagnoseCardSpace}, this scans every card through the
4622
+ * filters, so it's heavier than a normal replan. Intended for one-shot
4623
+ * out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
4624
+ *
4625
+ * @param opts.hints Optional ephemeral hints to apply after the filter chain.
4626
+ * @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
4627
+ * @param opts.threshold Min score to count as reachable (default 0.10).
4628
+ * @param opts.limit Optional cap on results (already sorted desc).
4629
+ */
4630
+ async forecast(opts) {
4631
+ const threshold = opts?.threshold ?? 0.1;
4632
+ const unseenOnly = opts?.unseenOnly ?? true;
4633
+ const courseId = this.course.getCourseID();
4634
+ const allCardIds = await this.course.getAllCardIds();
4635
+ let cards = allCardIds.map((cardId) => ({
4636
+ cardId,
4637
+ courseId,
4638
+ score: 1,
4639
+ provenance: []
4640
+ }));
4641
+ cards = await this.hydrateTags(cards);
4642
+ const fullPool = cards.slice();
4643
+ const context = await this.buildContext();
4644
+ for (const filter of this.filters) {
4645
+ cards = await filter.transform(cards, context);
4646
+ }
4647
+ if (opts?.hints) {
4648
+ cards = this.applyHints(cards, opts.hints, fullPool);
4649
+ }
4650
+ cards = cards.filter((c) => c.score >= threshold);
4651
+ if (unseenOnly) {
4652
+ let encountered;
4653
+ try {
4654
+ encountered = new Set(await this.user.getSeenCards(courseId));
4655
+ } catch {
4656
+ encountered = /* @__PURE__ */ new Set();
4657
+ }
4658
+ cards = cards.filter((c) => !encountered.has(c.cardId));
4659
+ }
4660
+ cards.sort((a, b) => b.score - a.score);
4661
+ return opts?.limit ? cards.slice(0, opts.limit) : cards;
4662
+ }
4512
4663
  /**
4513
4664
  * Scan every card in the course through the filter chain and report
4514
4665
  * how many are "well indicated" (score >= threshold) for the current user.
@@ -4773,6 +4924,7 @@ var init_3 = __esm({
4773
4924
  "./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
4774
4925
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
4775
4926
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
4927
+ "./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
4776
4928
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
4777
4929
  "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
4778
4930
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
@@ -4805,11 +4957,14 @@ __export(navigators_exports, {
4805
4957
  NavigatorRole: () => NavigatorRole,
4806
4958
  NavigatorRoles: () => NavigatorRoles,
4807
4959
  Navigators: () => Navigators,
4960
+ clearSrsBacklogDebug: () => clearSrsBacklogDebug,
4808
4961
  diversityRerank: () => diversityRerank,
4962
+ getActivePipeline: () => getActivePipeline,
4809
4963
  getCardOrigin: () => getCardOrigin,
4810
4964
  getRegisteredNavigator: () => getRegisteredNavigator,
4811
4965
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
4812
4966
  getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
4967
+ getSrsBacklogDebug: () => getSrsBacklogDebug,
4813
4968
  hasRegisteredNavigator: () => hasRegisteredNavigator,
4814
4969
  initializeNavigatorRegistry: () => initializeNavigatorRegistry,
4815
4970
  isFilter: () => isFilter,
@@ -4891,6 +5046,7 @@ var init_navigators = __esm({
4891
5046
  "use strict";
4892
5047
  init_diversityRerank();
4893
5048
  init_PipelineDebugger();
5049
+ init_SrsDebugger();
4894
5050
  init_logger();
4895
5051
  init_();
4896
5052
  init_2();