@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
@@ -593,6 +593,7 @@ __export(PipelineDebugger_exports, {
593
593
  buildRunReport: () => buildRunReport,
594
594
  captureRun: () => captureRun,
595
595
  clearRunHistory: () => clearRunHistory,
596
+ getActivePipeline: () => getActivePipeline,
596
597
  mountPipelineDebugger: () => mountPipelineDebugger,
597
598
  pipelineDebugAPI: () => pipelineDebugAPI,
598
599
  registerPipelineForDebug: () => registerPipelineForDebug
@@ -600,6 +601,9 @@ __export(PipelineDebugger_exports, {
600
601
  function registerPipelineForDebug(pipeline) {
601
602
  _activePipeline = pipeline;
602
603
  }
604
+ function getActivePipeline() {
605
+ return _activePipeline;
606
+ }
603
607
  function clearRunHistory() {
604
608
  runHistory.length = 0;
605
609
  }
@@ -1415,6 +1419,30 @@ Example:
1415
1419
  }
1416
1420
  });
1417
1421
 
1422
+ // src/core/navigators/SrsDebugger.ts
1423
+ var SrsDebugger_exports = {};
1424
+ __export(SrsDebugger_exports, {
1425
+ captureSrsBacklog: () => captureSrsBacklog,
1426
+ clearSrsBacklogDebug: () => clearSrsBacklogDebug,
1427
+ getSrsBacklogDebug: () => getSrsBacklogDebug
1428
+ });
1429
+ function captureSrsBacklog(snapshot) {
1430
+ snapshots.set(snapshot.courseId, snapshot);
1431
+ }
1432
+ function getSrsBacklogDebug() {
1433
+ return [...snapshots.values()].sort((a, b) => b.timestamp - a.timestamp);
1434
+ }
1435
+ function clearSrsBacklogDebug() {
1436
+ snapshots.clear();
1437
+ }
1438
+ var snapshots;
1439
+ var init_SrsDebugger = __esm({
1440
+ "src/core/navigators/SrsDebugger.ts"() {
1441
+ "use strict";
1442
+ snapshots = /* @__PURE__ */ new Map();
1443
+ }
1444
+ });
1445
+
1418
1446
  // src/core/navigators/generators/CompositeGenerator.ts
1419
1447
  var CompositeGenerator_exports = {};
1420
1448
  __export(CompositeGenerator_exports, {
@@ -1782,7 +1810,7 @@ function shuffleInPlace(arr) {
1782
1810
  function pickTopByScore(cards, limit) {
1783
1811
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1784
1812
  }
1785
- 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;
1813
+ 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;
1786
1814
  var init_prescribed = __esm({
1787
1815
  "src/core/navigators/generators/prescribed.ts"() {
1788
1816
  "use strict";
@@ -1799,6 +1827,9 @@ var init_prescribed = __esm({
1799
1827
  BASE_SUPPORT_SCORE = 0.8;
1800
1828
  DISCOVERED_SUPPORT_SCORE = 12;
1801
1829
  BASE_PRACTICE_SCORE = 1;
1830
+ PRACTICE_BASE_MULT = 2;
1831
+ MAX_PRACTICE_MULTIPLIER = 4;
1832
+ PRACTICE_STALENESS_BUMP_PER_DAY = 0.5;
1802
1833
  MAX_TARGET_MULTIPLIER = 8;
1803
1834
  MAX_SUPPORT_MULTIPLIER = 4;
1804
1835
  PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
@@ -1858,6 +1889,8 @@ var init_prescribed = __esm({
1858
1889
  const emitted = [];
1859
1890
  const emittedIds = /* @__PURE__ */ new Set();
1860
1891
  const groupRuntimes = [];
1892
+ const priorPracticeDebt = progress.practiceDebt ?? {};
1893
+ const nextPracticeDebt = {};
1861
1894
  for (const group of this.config.groups) {
1862
1895
  const runtime = this.buildGroupRuntimeState({
1863
1896
  group,
@@ -1915,10 +1948,13 @@ var init_prescribed = __esm({
1915
1948
  userTagElo,
1916
1949
  userGlobalElo,
1917
1950
  activeIds,
1918
- seenIds
1951
+ seenIds,
1952
+ priorPracticeDebt,
1953
+ nextPracticeDebt
1919
1954
  });
1920
1955
  emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
1921
1956
  }
1957
+ nextState.practiceDebt = nextPracticeDebt;
1922
1958
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1923
1959
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
1924
1960
  boostTags: hintSummary.boostTags,
@@ -2252,9 +2288,16 @@ var init_prescribed = __esm({
2252
2288
  * `practiceMinCount`), this resolves cards carrying that tag and emits them
2253
2289
  * into the candidate pool. It exists because global-ELO retrieval
2254
2290
  * systematically fails to fetch the (low-ELO) drill cards for a
2255
- * freshly-introduced skill — putting them in the pool here lets the pipeline's
2256
- * scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
2257
- * this method's job; it only guarantees presence.
2291
+ * freshly-introduced skill — putting them in the pool here guarantees presence.
2292
+ *
2293
+ * Emphasis is a **practice-debt pressure** (parallel to SRS backlog pressure):
2294
+ * cards score `base × multiplier`, where the multiplier starts at
2295
+ * PRACTICE_BASE_MULT (so a few reps land promptly post-intro, competing with
2296
+ * pressured reviews) and escalates by how long the debt has stayed open
2297
+ * (per-tag, time-based via `priorPracticeDebt`/`nextPracticeDebt`), clamped at
2298
+ * MAX_PRACTICE_MULTIPLIER. The debt is durable and self-discharges the instant
2299
+ * the skill reaches `practiceMinCount` — so this no longer relies on the
2300
+ * session-scoped intro boost to actually surface.
2258
2301
  *
2259
2302
  * Fully data-driven: the unlock relation comes from the hierarchy config and
2260
2303
  * practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
@@ -2269,7 +2312,9 @@ var init_prescribed = __esm({
2269
2312
  userTagElo,
2270
2313
  userGlobalElo,
2271
2314
  activeIds,
2272
- seenIds
2315
+ seenIds,
2316
+ priorPracticeDebt,
2317
+ nextPracticeDebt
2273
2318
  } = args;
2274
2319
  const patterns = group.practiceTagPatterns ?? [];
2275
2320
  if (patterns.length === 0) return [];
@@ -2279,6 +2324,20 @@ var init_prescribed = __esm({
2279
2324
  (tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
2280
2325
  );
2281
2326
  if (practiceTags.length === 0) return [];
2327
+ const now = Date.now();
2328
+ const DAY_MS = 24 * 60 * 60 * 1e3;
2329
+ const tagMultiplier = /* @__PURE__ */ new Map();
2330
+ for (const tag of practiceTags) {
2331
+ const firstOwedAt = priorPracticeDebt[tag] ?? isoNow();
2332
+ nextPracticeDebt[tag] = firstOwedAt;
2333
+ const staleDays = Math.max(0, (now - new Date(firstOwedAt).getTime()) / DAY_MS);
2334
+ const mult = clamp(
2335
+ PRACTICE_BASE_MULT + staleDays * PRACTICE_STALENESS_BUMP_PER_DAY,
2336
+ PRACTICE_BASE_MULT,
2337
+ MAX_PRACTICE_MULTIPLIER
2338
+ );
2339
+ tagMultiplier.set(tag, mult);
2340
+ }
2282
2341
  const practiceCardIds = this.findDiscoveredSupportCards({
2283
2342
  supportTags: practiceTags,
2284
2343
  cardsByTag,
@@ -2294,18 +2353,25 @@ var init_prescribed = __esm({
2294
2353
  const cards = [];
2295
2354
  for (const cardId of practiceCardIds) {
2296
2355
  emittedIds.add(cardId);
2356
+ let mult = PRACTICE_BASE_MULT;
2357
+ for (const tag of practiceTags) {
2358
+ if (cardsByTag.get(tag)?.includes(cardId) ?? false) {
2359
+ mult = Math.max(mult, tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT);
2360
+ }
2361
+ }
2362
+ const score = BASE_PRACTICE_SCORE * mult;
2297
2363
  cards.push({
2298
2364
  cardId,
2299
2365
  courseId,
2300
- score: BASE_PRACTICE_SCORE,
2366
+ score,
2301
2367
  provenance: [
2302
2368
  {
2303
2369
  strategy: "prescribed",
2304
2370
  strategyName: this.strategyName || this.name,
2305
2371
  strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2306
2372
  action: "generated",
2307
- score: BASE_PRACTICE_SCORE,
2308
- reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
2373
+ score,
2374
+ 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}`
2309
2375
  }
2310
2376
  ]
2311
2377
  });
@@ -2479,14 +2545,15 @@ __export(srs_exports, {
2479
2545
  default: () => SRSNavigator
2480
2546
  });
2481
2547
  import moment3 from "moment";
2482
- var DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_PRESSURE, SRSNavigator;
2548
+ var DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_MULTIPLIER, SRSNavigator;
2483
2549
  var init_srs = __esm({
2484
2550
  "src/core/navigators/generators/srs.ts"() {
2485
2551
  "use strict";
2486
2552
  init_navigators();
2553
+ init_SrsDebugger();
2487
2554
  init_logger();
2488
2555
  DEFAULT_HEALTHY_BACKLOG = 20;
2489
- MAX_BACKLOG_PRESSURE = 0.5;
2556
+ MAX_BACKLOG_MULTIPLIER = 2;
2490
2557
  SRSNavigator = class extends ContentNavigator {
2491
2558
  /** Human-readable name for CardGenerator interface */
2492
2559
  name;
@@ -2553,9 +2620,18 @@ var init_srs = __esm({
2553
2620
  }
2554
2621
  }
2555
2622
  }
2556
- const backlogPressure = this.computeBacklogPressure(dueReviews.length);
2623
+ const backlogMultiplier = this.computeBacklogMultiplier(dueReviews.length);
2624
+ const notDue = reviews.filter((r) => !now.isAfter(moment3.utc(r.reviewTime)));
2625
+ let nextDueIn = null;
2626
+ if (notDue.length > 0) {
2627
+ const next = notDue.reduce(
2628
+ (a, b) => moment3.utc(a.reviewTime).isBefore(moment3.utc(b.reviewTime)) ? a : b
2629
+ );
2630
+ const until = moment3.duration(moment3.utc(next.reviewTime).diff(now));
2631
+ nextDueIn = until.asHours() < 1 ? `${Math.round(until.asMinutes())}m` : until.asHours() < 24 ? `${Math.round(until.asHours())}h` : `${Math.round(until.asDays())}d`;
2632
+ }
2557
2633
  if (dueReviews.length > 0) {
2558
- const pressureNote = backlogPressure > 0 ? ` [backlog pressure: +${backlogPressure.toFixed(2)}]` : ` [healthy backlog]`;
2634
+ const pressureNote = backlogMultiplier > 1 ? ` [backlog pressure: \xD7${backlogMultiplier.toFixed(2)}]` : ` [healthy backlog]`;
2559
2635
  logger.info(
2560
2636
  `[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
2561
2637
  );
@@ -2574,7 +2650,7 @@ var init_srs = __esm({
2574
2650
  logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
2575
2651
  }
2576
2652
  const scored = dueReviews.map((review) => {
2577
- const { score, reason } = this.computeUrgencyScore(review, now, backlogPressure);
2653
+ const { score, reason } = this.computeUrgencyScore(review, now, backlogMultiplier);
2578
2654
  return {
2579
2655
  cardId: review.cardId,
2580
2656
  courseId: review.courseId,
@@ -2592,30 +2668,42 @@ var init_srs = __esm({
2592
2668
  ]
2593
2669
  };
2594
2670
  });
2595
- return { cards: scored.sort((a, b) => b.score - a.score).slice(0, limit) };
2671
+ const sorted = scored.sort((a, b) => b.score - a.score);
2672
+ captureSrsBacklog({
2673
+ courseId,
2674
+ scheduledTotal: reviews.length,
2675
+ dueNow: dueReviews.length,
2676
+ healthyBacklog: this.healthyBacklog,
2677
+ backlogMultiplier,
2678
+ maxBacklogMultiplier: MAX_BACKLOG_MULTIPLIER,
2679
+ topReviewScore: sorted.length > 0 ? sorted[0].score : null,
2680
+ nextDueIn,
2681
+ timestamp: Date.now()
2682
+ });
2683
+ return { cards: sorted.slice(0, limit) };
2596
2684
  }
2597
2685
  /**
2598
- * Compute backlog pressure based on number of due reviews.
2686
+ * Compute the multiplicative backlog pressure based on number of due reviews.
2599
2687
  *
2600
- * Backlog pressure is 0 when at or below healthy threshold,
2601
- * and increases linearly above it, maxing out at MAX_BACKLOG_PRESSURE.
2688
+ * ×1.0 at or below the healthy threshold (no boost), increasing linearly above
2689
+ * it and maxing out at MAX_BACKLOG_MULTIPLIER at the healthy backlog.
2602
2690
  *
2603
- * Examples (with default healthyBacklog=20):
2604
- * - 10 due reviews → 0.00 (healthy)
2605
- * - 20 due reviews → 0.00 (at threshold)
2606
- * - 40 due reviews → 0.25 (2x threshold)
2607
- * - 60 due reviews → 0.50 (3x threshold, maxed)
2691
+ * Examples (with default healthyBacklog=20, MAX_BACKLOG_MULTIPLIER=2.0):
2692
+ * - 10 due reviews → ×1.00 (healthy)
2693
+ * - 20 due reviews → ×1.00 (at threshold)
2694
+ * - 40 due reviews → ×1.50 (2x threshold)
2695
+ * - 60 due reviews → ×2.00 (3x threshold, maxed)
2608
2696
  *
2609
2697
  * @param dueCount - Number of reviews currently due
2610
- * @returns Backlog pressure score to add to urgency (0 to MAX_BACKLOG_PRESSURE)
2698
+ * @returns Multiplier applied to review urgency (1.0 to MAX_BACKLOG_MULTIPLIER)
2611
2699
  */
2612
- computeBacklogPressure(dueCount) {
2700
+ computeBacklogMultiplier(dueCount) {
2613
2701
  if (dueCount <= this.healthyBacklog) {
2614
- return 0;
2702
+ return 1;
2615
2703
  }
2616
2704
  const excess = dueCount - this.healthyBacklog;
2617
- const pressure = excess / this.healthyBacklog * (MAX_BACKLOG_PRESSURE / 2);
2618
- return Math.min(MAX_BACKLOG_PRESSURE, pressure);
2705
+ const multiplier = 1 + excess / this.healthyBacklog * ((MAX_BACKLOG_MULTIPLIER - 1) / 2);
2706
+ return Math.min(MAX_BACKLOG_MULTIPLIER, multiplier);
2619
2707
  }
2620
2708
  /**
2621
2709
  * Compute urgency score for a review card.
@@ -2630,19 +2718,20 @@ var init_srs = __esm({
2630
2718
  * - 30 days (720h) → ~0.56
2631
2719
  * - 180 days → ~0.30
2632
2720
  *
2633
- * 3. Backlog pressure = global boost when review backlog exceeds healthy threshold
2634
- * - At healthy backlog: 0
2635
- * - At 2x healthy: +0.25
2636
- * - At 3x+ healthy: +0.50 (max)
2721
+ * 3. Backlog pressure = global *multiplier* when review backlog exceeds the
2722
+ * healthy threshold (×1.0 healthy up to MAX_BACKLOG_MULTIPLIER at 3×).
2637
2723
  *
2638
- * Combined: base 0.5 + (urgency factors * 0.45) + backlog pressure
2639
- * Result range: 0.5 to 1.0 (uncapped to allow high-urgency reviews to compete with new cards)
2724
+ * Combined: (base 0.5 + urgency factors * 0.45) × backlog multiplier.
2725
+ * Per-card range before pressure: ~0.57–0.95. NOT clamped to 1.0 under a
2726
+ * heavy backlog reviews scale onto the open scale to compete with (and exceed)
2727
+ * new cards; what keeps them from running away is the bounded multiplier, not
2728
+ * a hard ceiling.
2640
2729
  *
2641
2730
  * @param review - The scheduled card to score
2642
2731
  * @param now - Current time
2643
- * @param backlogPressure - Pre-computed backlog pressure (0 to 0.5)
2732
+ * @param backlogMultiplier - Pre-computed backlog multiplier (1.0 to MAX_BACKLOG_MULTIPLIER)
2644
2733
  */
2645
- computeUrgencyScore(review, now, backlogPressure) {
2734
+ computeUrgencyScore(review, now, backlogMultiplier) {
2646
2735
  const scheduledAt = moment3.utc(review.scheduledAt);
2647
2736
  const due = moment3.utc(review.reviewTime);
2648
2737
  const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
@@ -2652,15 +2741,15 @@ var init_srs = __esm({
2652
2741
  const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
2653
2742
  const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
2654
2743
  const baseScore = 0.5 + urgency * 0.45;
2655
- const score = Math.min(1, baseScore + backlogPressure);
2744
+ const score = baseScore * backlogMultiplier;
2656
2745
  const reasonParts = [
2657
2746
  `${Math.round(hoursOverdue)}h overdue`,
2658
2747
  `interval: ${Math.round(intervalHours)}h`,
2659
2748
  `relative: ${relativeOverdue.toFixed(2)}`,
2660
2749
  `recency: ${recencyFactor.toFixed(2)}`
2661
2750
  ];
2662
- if (backlogPressure > 0) {
2663
- reasonParts.push(`backlog: +${backlogPressure.toFixed(2)}`);
2751
+ if (backlogMultiplier > 1) {
2752
+ reasonParts.push(`backlog: \xD7${backlogMultiplier.toFixed(2)}`);
2664
2753
  }
2665
2754
  reasonParts.push("review");
2666
2755
  const reason = reasonParts.join(", ");
@@ -4386,6 +4475,68 @@ var init_Pipeline = __esm({
4386
4475
  // ---------------------------------------------------------------------------
4387
4476
  // Card-space diagnostic
4388
4477
  // ---------------------------------------------------------------------------
4478
+ /**
4479
+ * Commit-free forecast: score the user's full card space through the filter
4480
+ * chain and return the cards that are currently *reachable* (score >=
4481
+ * threshold), optionally nudged by caller-supplied hints and/or restricted
4482
+ * to cards the user hasn't seen yet.
4483
+ *
4484
+ * This is a GENERIC primitive — it returns scored, tag-hydrated cards and
4485
+ * stops there. It has no knowledge of any particular tag convention; callers
4486
+ * decide what the surviving cards mean (e.g. filter to their own "intro"
4487
+ * tag family). Nothing is written and no session is started.
4488
+ *
4489
+ * The optional `hints` are the "out-of-band kick": they run through the same
4490
+ * {@link applyHints} path a live replan uses, so the two semantics carry over —
4491
+ * - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
4492
+ * stays out), and
4493
+ * - `requireTags`/`requireCards` inject from the full pre-filter pool,
4494
+ * *bypassing* gating (use when you want a card regardless of reachability).
4495
+ * Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
4496
+ * user has already seen — pass `unseenOnly: false` if that matters.
4497
+ *
4498
+ * Cost note: like {@link diagnoseCardSpace}, this scans every card through the
4499
+ * filters, so it's heavier than a normal replan. Intended for one-shot
4500
+ * out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
4501
+ *
4502
+ * @param opts.hints Optional ephemeral hints to apply after the filter chain.
4503
+ * @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
4504
+ * @param opts.threshold Min score to count as reachable (default 0.10).
4505
+ * @param opts.limit Optional cap on results (already sorted desc).
4506
+ */
4507
+ async forecast(opts) {
4508
+ const threshold = opts?.threshold ?? 0.1;
4509
+ const unseenOnly = opts?.unseenOnly ?? true;
4510
+ const courseId = this.course.getCourseID();
4511
+ const allCardIds = await this.course.getAllCardIds();
4512
+ let cards = allCardIds.map((cardId) => ({
4513
+ cardId,
4514
+ courseId,
4515
+ score: 1,
4516
+ provenance: []
4517
+ }));
4518
+ cards = await this.hydrateTags(cards);
4519
+ const fullPool = cards.slice();
4520
+ const context = await this.buildContext();
4521
+ for (const filter of this.filters) {
4522
+ cards = await filter.transform(cards, context);
4523
+ }
4524
+ if (opts?.hints) {
4525
+ cards = this.applyHints(cards, opts.hints, fullPool);
4526
+ }
4527
+ cards = cards.filter((c) => c.score >= threshold);
4528
+ if (unseenOnly) {
4529
+ let encountered;
4530
+ try {
4531
+ encountered = new Set(await this.user.getSeenCards(courseId));
4532
+ } catch {
4533
+ encountered = /* @__PURE__ */ new Set();
4534
+ }
4535
+ cards = cards.filter((c) => !encountered.has(c.cardId));
4536
+ }
4537
+ cards.sort((a, b) => b.score - a.score);
4538
+ return opts?.limit ? cards.slice(0, opts.limit) : cards;
4539
+ }
4389
4540
  /**
4390
4541
  * Scan every card in the course through the filter chain and report
4391
4542
  * how many are "well indicated" (score >= threshold) for the current user.
@@ -4650,6 +4801,7 @@ var init_3 = __esm({
4650
4801
  "./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
4651
4802
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
4652
4803
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
4804
+ "./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
4653
4805
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
4654
4806
  "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
4655
4807
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
@@ -4682,11 +4834,14 @@ __export(navigators_exports, {
4682
4834
  NavigatorRole: () => NavigatorRole,
4683
4835
  NavigatorRoles: () => NavigatorRoles,
4684
4836
  Navigators: () => Navigators,
4837
+ clearSrsBacklogDebug: () => clearSrsBacklogDebug,
4685
4838
  diversityRerank: () => diversityRerank,
4839
+ getActivePipeline: () => getActivePipeline,
4686
4840
  getCardOrigin: () => getCardOrigin,
4687
4841
  getRegisteredNavigator: () => getRegisteredNavigator,
4688
4842
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
4689
4843
  getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
4844
+ getSrsBacklogDebug: () => getSrsBacklogDebug,
4690
4845
  hasRegisteredNavigator: () => hasRegisteredNavigator,
4691
4846
  initializeNavigatorRegistry: () => initializeNavigatorRegistry,
4692
4847
  isFilter: () => isFilter,
@@ -4768,6 +4923,7 @@ var init_navigators = __esm({
4768
4923
  "use strict";
4769
4924
  init_diversityRerank();
4770
4925
  init_PipelineDebugger();
4926
+ init_SrsDebugger();
4771
4927
  init_logger();
4772
4928
  init_();
4773
4929
  init_2();