@vue-skuilder/db 0.1.24 → 0.1.25

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 (50) hide show
  1. package/dist/{contentSource-BotbOOfX.d.ts → contentSource-BmnmvH8C.d.ts} +41 -0
  2. package/dist/{contentSource-C90LH-OH.d.cts → contentSource-DfBbaLA-.d.cts} +41 -0
  3. package/dist/core/index.d.cts +94 -4
  4. package/dist/core/index.d.ts +94 -4
  5. package/dist/core/index.js +530 -83
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +528 -83
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-DGKp4zFB.d.cts → dataLayerProvider-BeRXVMs5.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-SBpz9jQf.d.ts → dataLayerProvider-CG9GfaAY.d.ts} +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 +526 -83
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +526 -83
  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 +526 -83
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +526 -83
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/index.d.cts +247 -14
  24. package/dist/index.d.ts +247 -14
  25. package/dist/index.js +1419 -140
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +1409 -137
  28. package/dist/index.mjs.map +1 -1
  29. package/docs/navigators-architecture.md +22 -4
  30. package/docs/todo-review-urgency-adaptation.md +205 -0
  31. package/package.json +3 -3
  32. package/src/core/interfaces/userDB.ts +44 -0
  33. package/src/core/navigators/Pipeline.ts +86 -5
  34. package/src/core/navigators/PipelineAssembler.ts +7 -21
  35. package/src/core/navigators/PipelineDebugger.ts +426 -0
  36. package/src/core/navigators/generators/CompositeGenerator.ts +21 -0
  37. package/src/core/navigators/generators/elo.ts +14 -1
  38. package/src/core/navigators/generators/srs.ts +146 -18
  39. package/src/core/navigators/index.ts +9 -0
  40. package/src/impl/couch/user-course-relDB.ts +12 -0
  41. package/src/study/MixerDebugger.ts +555 -0
  42. package/src/study/SessionController.ts +95 -19
  43. package/src/study/SessionDebugger.ts +442 -0
  44. package/src/study/SourceMixer.ts +36 -17
  45. package/src/study/TODO-session-scheduling.md +133 -0
  46. package/src/study/index.ts +2 -0
  47. package/src/study/services/EloService.ts +79 -4
  48. package/src/study/services/ResponseProcessor.ts +130 -72
  49. package/src/study/services/SrsService.ts +9 -0
  50. package/tests/core/navigators/PipelineAssembler.test.ts +4 -4
@@ -404,6 +404,15 @@ var init_user_course_relDB = __esm({
404
404
  void this.user.updateCourseSettings(this._courseId, updates);
405
405
  }
406
406
  }
407
+ async getStrategyState(strategyKey) {
408
+ return this.user.getStrategyState(this._courseId, strategyKey);
409
+ }
410
+ async putStrategyState(strategyKey, data) {
411
+ return this.user.putStrategyState(this._courseId, strategyKey, data);
412
+ }
413
+ async deleteStrategyState(strategyKey) {
414
+ return this.user.deleteStrategyState(this._courseId, strategyKey);
415
+ }
407
416
  async getReviewstoDate(targetDate) {
408
417
  const allReviews = await this.user.getPendingReviews(this._courseId);
409
418
  logger.debug(
@@ -473,6 +482,271 @@ var init_courseLookupDB = __esm({
473
482
  }
474
483
  });
475
484
 
485
+ // src/core/navigators/PipelineDebugger.ts
486
+ var PipelineDebugger_exports = {};
487
+ __export(PipelineDebugger_exports, {
488
+ buildRunReport: () => buildRunReport,
489
+ captureRun: () => captureRun,
490
+ mountPipelineDebugger: () => mountPipelineDebugger,
491
+ pipelineDebugAPI: () => pipelineDebugAPI
492
+ });
493
+ function getOrigin(card) {
494
+ const firstEntry = card.provenance[0];
495
+ if (!firstEntry) return "unknown";
496
+ const reason = firstEntry.reason?.toLowerCase() || "";
497
+ if (reason.includes("new card")) return "new";
498
+ if (reason.includes("review")) return "review";
499
+ return "unknown";
500
+ }
501
+ function captureRun(report) {
502
+ const fullReport = {
503
+ ...report,
504
+ runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
505
+ timestamp: /* @__PURE__ */ new Date()
506
+ };
507
+ runHistory.unshift(fullReport);
508
+ if (runHistory.length > MAX_RUNS) {
509
+ runHistory.pop();
510
+ }
511
+ }
512
+ function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards) {
513
+ const selectedIds = new Set(selectedCards.map((c) => c.cardId));
514
+ const cards = allCards.map((card) => ({
515
+ cardId: card.cardId,
516
+ courseId: card.courseId,
517
+ origin: getOrigin(card),
518
+ finalScore: card.score,
519
+ provenance: card.provenance,
520
+ selected: selectedIds.has(card.cardId)
521
+ }));
522
+ const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
523
+ const newSelected = selectedCards.filter((c) => getOrigin(c) === "new").length;
524
+ return {
525
+ courseId,
526
+ courseName,
527
+ generatorName,
528
+ generators,
529
+ generatedCount,
530
+ filters,
531
+ finalCount: selectedCards.length,
532
+ reviewsSelected,
533
+ newSelected,
534
+ cards
535
+ };
536
+ }
537
+ function formatProvenance(provenance) {
538
+ return provenance.map((p) => {
539
+ const actionSymbol = p.action === "generated" ? "\u{1F3B2}" : p.action === "boosted" ? "\u2B06\uFE0F" : p.action === "penalized" ? "\u2B07\uFE0F" : "\u27A1\uFE0F";
540
+ return ` ${actionSymbol} ${p.strategyName}: ${p.score.toFixed(3)} - ${p.reason}`;
541
+ }).join("\n");
542
+ }
543
+ function printRunSummary(run) {
544
+ console.group(`\u{1F50D} Pipeline Run: ${run.courseId} (${run.courseName || "unnamed"})`);
545
+ logger.info(`Run ID: ${run.runId}`);
546
+ logger.info(`Time: ${run.timestamp.toISOString()}`);
547
+ logger.info(`Generator: ${run.generatorName} \u2192 ${run.generatedCount} candidates`);
548
+ if (run.generators && run.generators.length > 0) {
549
+ console.group("Generator breakdown:");
550
+ for (const g of run.generators) {
551
+ logger.info(
552
+ ` ${g.name}: ${g.cardCount} cards (${g.newCount} new, ${g.reviewCount} reviews, top: ${g.topScore.toFixed(2)})`
553
+ );
554
+ }
555
+ console.groupEnd();
556
+ }
557
+ if (run.filters.length > 0) {
558
+ console.group("Filter impact:");
559
+ for (const f of run.filters) {
560
+ logger.info(` ${f.name}: \u2191${f.boosted} \u2193${f.penalized} =${f.passed} \u2715${f.removed}`);
561
+ }
562
+ console.groupEnd();
563
+ }
564
+ logger.info(
565
+ `Result: ${run.finalCount} cards selected (${run.newSelected} new, ${run.reviewsSelected} reviews)`
566
+ );
567
+ console.groupEnd();
568
+ }
569
+ function mountPipelineDebugger() {
570
+ if (typeof window === "undefined") return;
571
+ const win = window;
572
+ win.skuilder = win.skuilder || {};
573
+ win.skuilder.pipeline = pipelineDebugAPI;
574
+ }
575
+ var MAX_RUNS, runHistory, pipelineDebugAPI;
576
+ var init_PipelineDebugger = __esm({
577
+ "src/core/navigators/PipelineDebugger.ts"() {
578
+ "use strict";
579
+ init_logger();
580
+ MAX_RUNS = 10;
581
+ runHistory = [];
582
+ pipelineDebugAPI = {
583
+ /**
584
+ * Get raw run history for programmatic access.
585
+ */
586
+ get runs() {
587
+ return [...runHistory];
588
+ },
589
+ /**
590
+ * Show summary of a specific pipeline run.
591
+ */
592
+ showRun(idOrIndex = 0) {
593
+ if (runHistory.length === 0) {
594
+ logger.info("[Pipeline Debug] No runs captured yet.");
595
+ return;
596
+ }
597
+ let run;
598
+ if (typeof idOrIndex === "number") {
599
+ run = runHistory[idOrIndex];
600
+ if (!run) {
601
+ logger.info(
602
+ `[Pipeline Debug] No run found at index ${idOrIndex}. History length: ${runHistory.length}`
603
+ );
604
+ return;
605
+ }
606
+ } else {
607
+ run = runHistory.find((r) => r.runId.endsWith(idOrIndex));
608
+ if (!run) {
609
+ logger.info(`[Pipeline Debug] No run found matching ID '${idOrIndex}'.`);
610
+ return;
611
+ }
612
+ }
613
+ printRunSummary(run);
614
+ },
615
+ /**
616
+ * Show summary of the last pipeline run.
617
+ */
618
+ showLastRun() {
619
+ this.showRun(0);
620
+ },
621
+ /**
622
+ * Show detailed provenance for a specific card.
623
+ */
624
+ showCard(cardId) {
625
+ for (const run of runHistory) {
626
+ const card = run.cards.find((c) => c.cardId === cardId);
627
+ if (card) {
628
+ console.group(`\u{1F3B4} Card: ${cardId}`);
629
+ logger.info(`Course: ${card.courseId}`);
630
+ logger.info(`Origin: ${card.origin}`);
631
+ logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
632
+ logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
633
+ logger.info("Provenance:");
634
+ logger.info(formatProvenance(card.provenance));
635
+ console.groupEnd();
636
+ return;
637
+ }
638
+ }
639
+ logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
640
+ },
641
+ /**
642
+ * Explain why reviews may or may not have been selected.
643
+ */
644
+ explainReviews() {
645
+ if (runHistory.length === 0) {
646
+ logger.info("[Pipeline Debug] No runs captured yet.");
647
+ return;
648
+ }
649
+ console.group("\u{1F4CB} Review Selection Analysis");
650
+ for (const run of runHistory) {
651
+ console.group(`Run: ${run.courseId} @ ${run.timestamp.toLocaleTimeString()}`);
652
+ const allReviews = run.cards.filter((c) => c.origin === "review");
653
+ const selectedReviews = allReviews.filter((c) => c.selected);
654
+ if (allReviews.length === 0) {
655
+ logger.info("\u274C No reviews were generated. Check SRS logs for why.");
656
+ } else if (selectedReviews.length === 0) {
657
+ logger.info(`\u26A0\uFE0F ${allReviews.length} reviews generated but none selected.`);
658
+ logger.info("Possible reasons:");
659
+ const topNewScore = Math.max(
660
+ ...run.cards.filter((c) => c.origin === "new" && c.selected).map((c) => c.finalScore),
661
+ 0
662
+ );
663
+ const topReviewScore = Math.max(...allReviews.map((c) => c.finalScore), 0);
664
+ if (topReviewScore < topNewScore) {
665
+ logger.info(
666
+ ` - New cards scored higher (top new: ${topNewScore.toFixed(2)}, top review: ${topReviewScore.toFixed(2)})`
667
+ );
668
+ }
669
+ const topReview = allReviews.sort((a, b) => b.finalScore - a.finalScore)[0];
670
+ if (topReview) {
671
+ logger.info(` - Top review score: ${topReview.finalScore.toFixed(3)}`);
672
+ logger.info(" - Its provenance:");
673
+ logger.info(formatProvenance(topReview.provenance));
674
+ }
675
+ } else {
676
+ logger.info(`\u2705 ${selectedReviews.length}/${allReviews.length} reviews selected.`);
677
+ logger.info("Top selected review:");
678
+ const topSelected = selectedReviews.sort((a, b) => b.finalScore - a.finalScore)[0];
679
+ logger.info(formatProvenance(topSelected.provenance));
680
+ }
681
+ console.groupEnd();
682
+ }
683
+ console.groupEnd();
684
+ },
685
+ /**
686
+ * Show all runs in compact format.
687
+ */
688
+ listRuns() {
689
+ if (runHistory.length === 0) {
690
+ logger.info("[Pipeline Debug] No runs captured yet.");
691
+ return;
692
+ }
693
+ console.table(
694
+ runHistory.map((r) => ({
695
+ id: r.runId.slice(-8),
696
+ time: r.timestamp.toLocaleTimeString(),
697
+ course: r.courseName || r.courseId.slice(0, 8),
698
+ generated: r.generatedCount,
699
+ selected: r.finalCount,
700
+ new: r.newSelected,
701
+ reviews: r.reviewsSelected
702
+ }))
703
+ );
704
+ },
705
+ /**
706
+ * Export run history as JSON for bug reports.
707
+ */
708
+ export() {
709
+ const json = JSON.stringify(runHistory, null, 2);
710
+ logger.info("[Pipeline Debug] Run history exported. Copy the returned string or use:");
711
+ logger.info(" copy(window.skuilder.pipeline.export())");
712
+ return json;
713
+ },
714
+ /**
715
+ * Clear run history.
716
+ */
717
+ clear() {
718
+ runHistory.length = 0;
719
+ logger.info("[Pipeline Debug] Run history cleared.");
720
+ },
721
+ /**
722
+ * Show help.
723
+ */
724
+ help() {
725
+ logger.info(`
726
+ \u{1F527} Pipeline Debug API
727
+
728
+ Commands:
729
+ .showLastRun() Show summary of most recent pipeline run
730
+ .showRun(id|index) Show summary of a specific run (by index or ID suffix)
731
+ .showCard(cardId) Show provenance trail for a specific card
732
+ .explainReviews() Analyze why reviews were/weren't selected
733
+ .listRuns() List all captured runs in table format
734
+ .export() Export run history as JSON for bug reports
735
+ .clear() Clear run history
736
+ .runs Access raw run history array
737
+ .help() Show this help message
738
+
739
+ Example:
740
+ window.skuilder.pipeline.showLastRun()
741
+ window.skuilder.pipeline.showRun(1)
742
+ window.skuilder.pipeline.showCard('abc123')
743
+ `);
744
+ }
745
+ };
746
+ mountPipelineDebugger();
747
+ }
748
+ });
749
+
476
750
  // src/core/navigators/generators/CompositeGenerator.ts
477
751
  var CompositeGenerator_exports = {};
478
752
  __export(CompositeGenerator_exports, {
@@ -541,6 +815,24 @@ var init_CompositeGenerator = __esm({
541
815
  const results = await Promise.all(
542
816
  this.generators.map((g) => g.getWeightedCards(limit, context))
543
817
  );
818
+ const generatorSummaries = [];
819
+ results.forEach((cards, index) => {
820
+ const gen = this.generators[index];
821
+ const genName = gen.name || `Generator ${index}`;
822
+ const newCards = cards.filter((c) => c.provenance[0]?.reason?.includes("new card"));
823
+ const reviewCards = cards.filter((c) => c.provenance[0]?.reason?.includes("review"));
824
+ if (cards.length > 0) {
825
+ const topScore = Math.max(...cards.map((c) => c.score)).toFixed(2);
826
+ const parts = [];
827
+ if (newCards.length > 0) parts.push(`${newCards.length} new`);
828
+ if (reviewCards.length > 0) parts.push(`${reviewCards.length} reviews`);
829
+ const breakdown = parts.length > 0 ? parts.join(", ") : `${cards.length} cards`;
830
+ generatorSummaries.push(`${genName}: ${breakdown} (top: ${topScore})`);
831
+ } else {
832
+ generatorSummaries.push(`${genName}: 0 cards`);
833
+ }
834
+ });
835
+ logger.info(`[Composite] Generator breakdown: ${generatorSummaries.join(" | ")}`);
544
836
  const byCardId = /* @__PURE__ */ new Map();
545
837
  results.forEach((cards, index) => {
546
838
  const gen = this.generators[index];
@@ -658,6 +950,7 @@ var init_elo = __esm({
658
950
  "src/core/navigators/generators/elo.ts"() {
659
951
  "use strict";
660
952
  init_navigators();
953
+ init_logger();
661
954
  ELONavigator = class extends ContentNavigator {
662
955
  /** Human-readable name for CardGenerator interface */
663
956
  name;
@@ -717,7 +1010,16 @@ var init_elo = __esm({
717
1010
  };
718
1011
  });
719
1012
  scored.sort((a, b) => b.score - a.score);
720
- return scored.slice(0, limit);
1013
+ const result = scored.slice(0, limit);
1014
+ if (result.length > 0) {
1015
+ const topScores = result.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ");
1016
+ logger.info(
1017
+ `[ELO] Course ${this.course.getCourseID()}: ${result.length} new cards (top scores: ${topScores})`
1018
+ );
1019
+ } else {
1020
+ logger.info(`[ELO] Course ${this.course.getCourseID()}: No new cards available`);
1021
+ }
1022
+ return result;
721
1023
  }
722
1024
  };
723
1025
  }
@@ -737,18 +1039,36 @@ __export(srs_exports, {
737
1039
  default: () => SRSNavigator
738
1040
  });
739
1041
  import moment3 from "moment";
740
- var SRSNavigator;
1042
+ var DEFAULT_HEALTHY_BACKLOG, MAX_BACKLOG_PRESSURE, SRSNavigator;
741
1043
  var init_srs = __esm({
742
1044
  "src/core/navigators/generators/srs.ts"() {
743
1045
  "use strict";
744
1046
  init_navigators();
745
1047
  init_logger();
1048
+ DEFAULT_HEALTHY_BACKLOG = 20;
1049
+ MAX_BACKLOG_PRESSURE = 0.5;
746
1050
  SRSNavigator = class extends ContentNavigator {
747
1051
  /** Human-readable name for CardGenerator interface */
748
1052
  name;
1053
+ /** Healthy backlog threshold - when exceeded, backlog pressure kicks in */
1054
+ healthyBacklog;
749
1055
  constructor(user, course, strategyData) {
750
1056
  super(user, course, strategyData);
751
1057
  this.name = strategyData?.name || "SRS";
1058
+ const config = this.parseConfig(strategyData?.serializedData);
1059
+ this.healthyBacklog = config.healthyBacklog ?? DEFAULT_HEALTHY_BACKLOG;
1060
+ }
1061
+ /**
1062
+ * Parse configuration from serialized JSON.
1063
+ */
1064
+ parseConfig(serializedData) {
1065
+ if (!serializedData) return {};
1066
+ try {
1067
+ return JSON.parse(serializedData);
1068
+ } catch {
1069
+ logger.warn("[SRS] Failed to parse strategy config, using defaults");
1070
+ return {};
1071
+ }
752
1072
  }
753
1073
  /**
754
1074
  * Get review cards scored by urgency.
@@ -756,6 +1076,7 @@ var init_srs = __esm({
756
1076
  * Score formula combines:
757
1077
  * - Relative overdueness: hoursOverdue / intervalHours
758
1078
  * - Interval recency: exponential decay favoring shorter intervals
1079
+ * - Backlog pressure: boost when due reviews exceed healthy threshold
759
1080
  *
760
1081
  * Cards not yet due are excluded (not scored as 0).
761
1082
  *
@@ -769,11 +1090,32 @@ var init_srs = __esm({
769
1090
  if (!this.user || !this.course) {
770
1091
  throw new Error("SRSNavigator requires user and course to be set");
771
1092
  }
772
- const reviews = await this.user.getPendingReviews(this.course.getCourseID());
1093
+ const courseId = this.course.getCourseID();
1094
+ const reviews = await this.user.getPendingReviews(courseId);
773
1095
  const now = moment3.utc();
774
1096
  const dueReviews = reviews.filter((r) => now.isAfter(moment3.utc(r.reviewTime)));
1097
+ const backlogPressure = this.computeBacklogPressure(dueReviews.length);
1098
+ if (dueReviews.length > 0) {
1099
+ const pressureNote = backlogPressure > 0 ? ` [backlog pressure: +${backlogPressure.toFixed(2)}]` : ` [healthy backlog]`;
1100
+ logger.info(
1101
+ `[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
1102
+ );
1103
+ } else if (reviews.length > 0) {
1104
+ const sortedByDue = [...reviews].sort(
1105
+ (a, b) => moment3.utc(a.reviewTime).diff(moment3.utc(b.reviewTime))
1106
+ );
1107
+ const nextDue = sortedByDue[0];
1108
+ const nextDueTime = moment3.utc(nextDue.reviewTime);
1109
+ const untilDue = moment3.duration(nextDueTime.diff(now));
1110
+ const untilDueStr = untilDue.asHours() < 1 ? `${Math.round(untilDue.asMinutes())}m` : untilDue.asHours() < 24 ? `${Math.round(untilDue.asHours())}h` : `${Math.round(untilDue.asDays())}d`;
1111
+ logger.info(
1112
+ `[SRS] Course ${courseId}: 0 reviews due now (${reviews.length} scheduled, next in ${untilDueStr})`
1113
+ );
1114
+ } else {
1115
+ logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
1116
+ }
775
1117
  const scored = dueReviews.map((review) => {
776
- const { score, reason } = this.computeUrgencyScore(review, now);
1118
+ const { score, reason } = this.computeUrgencyScore(review, now, backlogPressure);
777
1119
  return {
778
1120
  cardId: review.cardId,
779
1121
  courseId: review.courseId,
@@ -791,13 +1133,35 @@ var init_srs = __esm({
791
1133
  ]
792
1134
  };
793
1135
  });
794
- logger.debug(`[srsNav] got ${scored.length} weighted cards`);
795
1136
  return scored.sort((a, b) => b.score - a.score).slice(0, limit);
796
1137
  }
1138
+ /**
1139
+ * Compute backlog pressure based on number of due reviews.
1140
+ *
1141
+ * Backlog pressure is 0 when at or below healthy threshold,
1142
+ * and increases linearly above it, maxing out at MAX_BACKLOG_PRESSURE.
1143
+ *
1144
+ * Examples (with default healthyBacklog=20):
1145
+ * - 10 due reviews → 0.00 (healthy)
1146
+ * - 20 due reviews → 0.00 (at threshold)
1147
+ * - 40 due reviews → 0.25 (2x threshold)
1148
+ * - 60 due reviews → 0.50 (3x threshold, maxed)
1149
+ *
1150
+ * @param dueCount - Number of reviews currently due
1151
+ * @returns Backlog pressure score to add to urgency (0 to MAX_BACKLOG_PRESSURE)
1152
+ */
1153
+ computeBacklogPressure(dueCount) {
1154
+ if (dueCount <= this.healthyBacklog) {
1155
+ return 0;
1156
+ }
1157
+ const excess = dueCount - this.healthyBacklog;
1158
+ const pressure = excess / this.healthyBacklog * (MAX_BACKLOG_PRESSURE / 2);
1159
+ return Math.min(MAX_BACKLOG_PRESSURE, pressure);
1160
+ }
797
1161
  /**
798
1162
  * Compute urgency score for a review card.
799
1163
  *
800
- * Two factors:
1164
+ * Three factors:
801
1165
  * 1. Relative overdueness = hoursOverdue / intervalHours
802
1166
  * - 2 days overdue on 3-day interval = 0.67 (urgent)
803
1167
  * - 2 days overdue on 180-day interval = 0.01 (not urgent)
@@ -807,10 +1171,19 @@ var init_srs = __esm({
807
1171
  * - 30 days (720h) → ~0.56
808
1172
  * - 180 days → ~0.30
809
1173
  *
810
- * Combined: base 0.5 + weighted average of factors * 0.45
811
- * Result range: approximately 0.5 to 0.95
1174
+ * 3. Backlog pressure = global boost when review backlog exceeds healthy threshold
1175
+ * - At healthy backlog: 0
1176
+ * - At 2x healthy: +0.25
1177
+ * - At 3x+ healthy: +0.50 (max)
1178
+ *
1179
+ * Combined: base 0.5 + (urgency factors * 0.45) + backlog pressure
1180
+ * Result range: 0.5 to 1.0 (uncapped to allow high-urgency reviews to compete with new cards)
1181
+ *
1182
+ * @param review - The scheduled card to score
1183
+ * @param now - Current time
1184
+ * @param backlogPressure - Pre-computed backlog pressure (0 to 0.5)
812
1185
  */
813
- computeUrgencyScore(review, now) {
1186
+ computeUrgencyScore(review, now, backlogPressure) {
814
1187
  const scheduledAt = moment3.utc(review.scheduledAt);
815
1188
  const due = moment3.utc(review.reviewTime);
816
1189
  const intervalHours = Math.max(1, due.diff(scheduledAt, "hours"));
@@ -819,8 +1192,19 @@ var init_srs = __esm({
819
1192
  const recencyFactor = 0.3 + 0.7 * Math.exp(-intervalHours / 720);
820
1193
  const overdueContribution = Math.min(1, Math.max(0, relativeOverdue));
821
1194
  const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
822
- const score = Math.min(0.95, 0.5 + urgency * 0.45);
823
- const reason = `${Math.round(hoursOverdue)}h overdue (interval: ${Math.round(intervalHours)}h, relative: ${relativeOverdue.toFixed(2)}), recency: ${recencyFactor.toFixed(2)}, review`;
1195
+ const baseScore = 0.5 + urgency * 0.45;
1196
+ const score = Math.min(1, baseScore + backlogPressure);
1197
+ const reasonParts = [
1198
+ `${Math.round(hoursOverdue)}h overdue`,
1199
+ `interval: ${Math.round(intervalHours)}h`,
1200
+ `relative: ${relativeOverdue.toFixed(2)}`,
1201
+ `recency: ${recencyFactor.toFixed(2)}`
1202
+ ];
1203
+ if (backlogPressure > 0) {
1204
+ reasonParts.push(`backlog: +${backlogPressure.toFixed(2)}`);
1205
+ }
1206
+ reasonParts.push("review");
1207
+ const reason = reasonParts.join(", ");
824
1208
  return { score, reason };
825
1209
  }
826
1210
  };
@@ -1851,10 +2235,23 @@ function logTagHydration(cards, tagsByCard) {
1851
2235
  `[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
1852
2236
  );
1853
2237
  }
1854
- function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
2238
+ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores, filterImpacts) {
1855
2239
  const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
2240
+ let filterSummary = "";
2241
+ if (filterImpacts.length > 0) {
2242
+ const impacts = filterImpacts.map((f) => {
2243
+ const parts = [];
2244
+ if (f.boosted > 0) parts.push(`+${f.boosted}`);
2245
+ if (f.penalized > 0) parts.push(`-${f.penalized}`);
2246
+ if (f.passed > 0) parts.push(`=${f.passed}`);
2247
+ return `${f.name}: ${parts.join("/")}`;
2248
+ });
2249
+ filterSummary = `
2250
+ Filter impact: ${impacts.join(", ")}`;
2251
+ }
1856
2252
  logger.info(
1857
- `[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
2253
+ `[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})` + filterSummary + `
2254
+ \u{1F4A1} Inspect: window.skuilder.pipeline`
1858
2255
  );
1859
2256
  }
1860
2257
  function logCardProvenance(cards, maxCards = 3) {
@@ -1878,6 +2275,7 @@ var init_Pipeline = __esm({
1878
2275
  init_navigators();
1879
2276
  init_logger();
1880
2277
  init_orchestration();
2278
+ init_PipelineDebugger();
1881
2279
  Pipeline = class extends ContentNavigator {
1882
2280
  generator;
1883
2281
  filters;
@@ -1925,12 +2323,49 @@ var init_Pipeline = __esm({
1925
2323
  );
1926
2324
  let cards = await this.generator.getWeightedCards(fetchLimit, context);
1927
2325
  const generatedCount = cards.length;
2326
+ let generatorSummaries;
2327
+ if (this.generator.generators) {
2328
+ const genMap = /* @__PURE__ */ new Map();
2329
+ for (const card of cards) {
2330
+ const firstProv = card.provenance[0];
2331
+ if (firstProv) {
2332
+ const genName = firstProv.strategyName;
2333
+ if (!genMap.has(genName)) {
2334
+ genMap.set(genName, { cards: [] });
2335
+ }
2336
+ genMap.get(genName).cards.push(card);
2337
+ }
2338
+ }
2339
+ generatorSummaries = Array.from(genMap.entries()).map(([name, data]) => {
2340
+ const newCards = data.cards.filter((c) => c.provenance[0]?.reason?.includes("new card"));
2341
+ const reviewCards = data.cards.filter((c) => c.provenance[0]?.reason?.includes("review"));
2342
+ return {
2343
+ name,
2344
+ cardCount: data.cards.length,
2345
+ newCount: newCards.length,
2346
+ reviewCount: reviewCards.length,
2347
+ topScore: Math.max(...data.cards.map((c) => c.score), 0)
2348
+ };
2349
+ });
2350
+ }
1928
2351
  logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
1929
2352
  cards = await this.hydrateTags(cards);
2353
+ const allCardsBeforeFiltering = [...cards];
2354
+ const filterImpacts = [];
1930
2355
  for (const filter of this.filters) {
1931
2356
  const beforeCount = cards.length;
2357
+ const beforeScores = new Map(cards.map((c) => [c.cardId, c.score]));
1932
2358
  cards = await filter.transform(cards, context);
1933
- logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeCount} \u2192 ${cards.length} cards`);
2359
+ let boosted = 0, penalized = 0, passed = 0;
2360
+ const removed = beforeCount - cards.length;
2361
+ for (const card of cards) {
2362
+ const before = beforeScores.get(card.cardId) ?? 0;
2363
+ if (card.score > before) boosted++;
2364
+ else if (card.score < before) penalized++;
2365
+ else passed++;
2366
+ }
2367
+ filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
2368
+ logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
1934
2369
  }
1935
2370
  cards = cards.filter((c) => c.score > 0);
1936
2371
  cards.sort((a, b) => b.score - a.score);
@@ -1941,9 +2376,26 @@ var init_Pipeline = __esm({
1941
2376
  generatedCount,
1942
2377
  this.filters.length,
1943
2378
  result.length,
1944
- topScores
2379
+ topScores,
2380
+ filterImpacts
1945
2381
  );
1946
2382
  logCardProvenance(result, 3);
2383
+ try {
2384
+ const courseName = await this.course?.getCourseConfig().then((c) => c.name).catch(() => void 0);
2385
+ const report = buildRunReport(
2386
+ this.course?.getCourseID() || "unknown",
2387
+ courseName,
2388
+ this.generator.name,
2389
+ generatorSummaries,
2390
+ generatedCount,
2391
+ filterImpacts,
2392
+ allCardsBeforeFiltering,
2393
+ result
2394
+ );
2395
+ captureRun(report);
2396
+ } catch (e) {
2397
+ logger.debug(`[Pipeline] Failed to capture debug run: ${e}`);
2398
+ }
1947
2399
  return result;
1948
2400
  }
1949
2401
  /**
@@ -2033,6 +2485,56 @@ var init_Pipeline = __esm({
2033
2485
  }
2034
2486
  });
2035
2487
 
2488
+ // src/core/navigators/defaults.ts
2489
+ var defaults_exports = {};
2490
+ __export(defaults_exports, {
2491
+ createDefaultEloStrategy: () => createDefaultEloStrategy,
2492
+ createDefaultPipeline: () => createDefaultPipeline,
2493
+ createDefaultSrsStrategy: () => createDefaultSrsStrategy
2494
+ });
2495
+ function createDefaultEloStrategy(courseId) {
2496
+ return {
2497
+ _id: "NAVIGATION_STRATEGY-ELO-default",
2498
+ docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
2499
+ name: "ELO (default)",
2500
+ description: "Default ELO-based navigation strategy for new cards",
2501
+ implementingClass: "elo" /* ELO */,
2502
+ course: courseId,
2503
+ serializedData: ""
2504
+ };
2505
+ }
2506
+ function createDefaultSrsStrategy(courseId) {
2507
+ return {
2508
+ _id: "NAVIGATION_STRATEGY-SRS-default",
2509
+ docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
2510
+ name: "SRS (default)",
2511
+ description: "Default SRS-based navigation strategy for reviews",
2512
+ implementingClass: "srs" /* SRS */,
2513
+ course: courseId,
2514
+ serializedData: ""
2515
+ };
2516
+ }
2517
+ function createDefaultPipeline(user, course) {
2518
+ const courseId = course.getCourseID();
2519
+ const eloNavigator = new ELONavigator(user, course, createDefaultEloStrategy(courseId));
2520
+ const srsNavigator = new SRSNavigator(user, course, createDefaultSrsStrategy(courseId));
2521
+ const compositeGenerator = new CompositeGenerator([eloNavigator, srsNavigator]);
2522
+ const eloDistanceFilter = createEloDistanceFilter();
2523
+ return new Pipeline(compositeGenerator, [eloDistanceFilter], user, course);
2524
+ }
2525
+ var init_defaults = __esm({
2526
+ "src/core/navigators/defaults.ts"() {
2527
+ "use strict";
2528
+ init_navigators();
2529
+ init_Pipeline();
2530
+ init_CompositeGenerator();
2531
+ init_elo();
2532
+ init_srs();
2533
+ init_eloDistance();
2534
+ init_types_legacy();
2535
+ }
2536
+ });
2537
+
2036
2538
  // src/core/navigators/PipelineAssembler.ts
2037
2539
  var PipelineAssembler_exports = {};
2038
2540
  __export(PipelineAssembler_exports, {
@@ -2045,9 +2547,9 @@ var init_PipelineAssembler = __esm({
2045
2547
  init_navigators();
2046
2548
  init_WeightedFilter();
2047
2549
  init_Pipeline();
2048
- init_types_legacy();
2049
2550
  init_logger();
2050
2551
  init_CompositeGenerator();
2552
+ init_defaults();
2051
2553
  PipelineAssembler = class {
2052
2554
  /**
2053
2555
  * Assembles a navigation pipeline from strategy documents.
@@ -2086,9 +2588,11 @@ var init_PipelineAssembler = __esm({
2086
2588
  if (generatorStrategies.length === 0) {
2087
2589
  if (filterStrategies.length > 0) {
2088
2590
  logger.debug(
2089
- "[PipelineAssembler] No generator found, using default ELO with configured filters"
2591
+ "[PipelineAssembler] No generator found, using default ELO and SRS with configured filters"
2090
2592
  );
2091
- generatorStrategies.push(this.makeDefaultEloStrategy(course.getCourseID()));
2593
+ const courseId = course.getCourseID();
2594
+ generatorStrategies.push(createDefaultEloStrategy(courseId));
2595
+ generatorStrategies.push(createDefaultSrsStrategy(courseId));
2092
2596
  } else {
2093
2597
  warnings.push("No generator strategy found");
2094
2598
  return {
@@ -2149,75 +2653,10 @@ var init_PipelineAssembler = __esm({
2149
2653
  warnings
2150
2654
  };
2151
2655
  }
2152
- /**
2153
- * Creates a default ELO generator strategy.
2154
- * Used when filters are configured but no generator is specified.
2155
- */
2156
- makeDefaultEloStrategy(courseId) {
2157
- return {
2158
- _id: "NAVIGATION_STRATEGY-ELO-default",
2159
- course: courseId,
2160
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
2161
- name: "ELO (default)",
2162
- description: "Default ELO-based generator",
2163
- implementingClass: "elo" /* ELO */,
2164
- serializedData: ""
2165
- };
2166
- }
2167
2656
  };
2168
2657
  }
2169
2658
  });
2170
2659
 
2171
- // src/core/navigators/defaults.ts
2172
- var defaults_exports = {};
2173
- __export(defaults_exports, {
2174
- createDefaultEloStrategy: () => createDefaultEloStrategy,
2175
- createDefaultPipeline: () => createDefaultPipeline,
2176
- createDefaultSrsStrategy: () => createDefaultSrsStrategy
2177
- });
2178
- function createDefaultEloStrategy(courseId) {
2179
- return {
2180
- _id: "NAVIGATION_STRATEGY-ELO-default",
2181
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
2182
- name: "ELO (default)",
2183
- description: "Default ELO-based navigation strategy for new cards",
2184
- implementingClass: "elo" /* ELO */,
2185
- course: courseId,
2186
- serializedData: ""
2187
- };
2188
- }
2189
- function createDefaultSrsStrategy(courseId) {
2190
- return {
2191
- _id: "NAVIGATION_STRATEGY-SRS-default",
2192
- docType: "NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */,
2193
- name: "SRS (default)",
2194
- description: "Default SRS-based navigation strategy for reviews",
2195
- implementingClass: "srs" /* SRS */,
2196
- course: courseId,
2197
- serializedData: ""
2198
- };
2199
- }
2200
- function createDefaultPipeline(user, course) {
2201
- const courseId = course.getCourseID();
2202
- const eloNavigator = new ELONavigator(user, course, createDefaultEloStrategy(courseId));
2203
- const srsNavigator = new SRSNavigator(user, course, createDefaultSrsStrategy(courseId));
2204
- const compositeGenerator = new CompositeGenerator([eloNavigator, srsNavigator]);
2205
- const eloDistanceFilter = createEloDistanceFilter();
2206
- return new Pipeline(compositeGenerator, [eloDistanceFilter], user, course);
2207
- }
2208
- var init_defaults = __esm({
2209
- "src/core/navigators/defaults.ts"() {
2210
- "use strict";
2211
- init_navigators();
2212
- init_Pipeline();
2213
- init_CompositeGenerator();
2214
- init_elo();
2215
- init_srs();
2216
- init_eloDistance();
2217
- init_types_legacy();
2218
- }
2219
- });
2220
-
2221
2660
  // import("./**/*") in src/core/navigators/index.ts
2222
2661
  var globImport;
2223
2662
  var init_3 = __esm({
@@ -2225,6 +2664,7 @@ var init_3 = __esm({
2225
2664
  globImport = __glob({
2226
2665
  "./Pipeline.ts": () => Promise.resolve().then(() => (init_Pipeline(), Pipeline_exports)),
2227
2666
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
2667
+ "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
2228
2668
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
2229
2669
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
2230
2670
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
@@ -2260,6 +2700,8 @@ __export(navigators_exports, {
2260
2700
  initializeNavigatorRegistry: () => initializeNavigatorRegistry,
2261
2701
  isFilter: () => isFilter,
2262
2702
  isGenerator: () => isGenerator,
2703
+ mountPipelineDebugger: () => mountPipelineDebugger,
2704
+ pipelineDebugAPI: () => pipelineDebugAPI,
2263
2705
  registerNavigator: () => registerNavigator
2264
2706
  });
2265
2707
  function registerNavigator(implementingClass, constructor) {
@@ -2326,6 +2768,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
2326
2768
  var init_navigators = __esm({
2327
2769
  "src/core/navigators/index.ts"() {
2328
2770
  "use strict";
2771
+ init_PipelineDebugger();
2329
2772
  init_logger();
2330
2773
  init_();
2331
2774
  init_2();