@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
@@ -14,26 +14,49 @@ import { logger } from '@db/util/logger';
14
14
  //
15
15
  // A generator strategy that scores review cards by urgency.
16
16
  //
17
- // Urgency is determined by two factors:
17
+ // Urgency is determined by three factors:
18
18
  // 1. Overdueness - how far past the scheduled review time
19
19
  // 2. Interval recency - shorter scheduled intervals indicate "novel content in progress"
20
+ // 3. Backlog pressure - when too many reviews pile up, urgency increases globally
20
21
  //
21
22
  // A card with a 3-day interval that's 2 days overdue is more urgent than a card
22
23
  // with a 6-month interval that's 2 days overdue. The shorter interval represents
23
24
  // active learning at higher resolution.
24
25
  //
26
+ // DESIGN PHILOSOPHY: SRS scheduling times are "eligibility dates" not hard "due dates".
27
+ // When a card becomes eligible, it is "okish" to review now, but reviewing a little
28
+ // later may be optimal. We don't aim to always beat review queues to zero (death spiral),
29
+ // but rather maintain a healthy backlog of eligible reviews so the system can gracefully
30
+ // handle usage upticks or breaks.
31
+ //
25
32
  // This navigator only handles reviews - it does not generate new cards.
26
33
  // For new cards, use ELONavigator or another generator via CompositeGenerator.
27
34
  //
28
35
  // ============================================================================
29
36
 
37
+ /**
38
+ * Default healthy backlog size.
39
+ * When due reviews exceed this, backlog pressure kicks in.
40
+ * Can be overridden via strategy config.
41
+ */
42
+ const DEFAULT_HEALTHY_BACKLOG = 20;
43
+
44
+ /**
45
+ * Maximum backlog pressure contribution to score.
46
+ * At 3x healthy backlog, pressure maxes out.
47
+ */
48
+ const MAX_BACKLOG_PRESSURE = 0.5;
49
+
30
50
  /**
31
51
  * Configuration for the SRS strategy.
32
- * Currently minimal - the algorithm is not parameterized.
33
52
  */
34
- // eslint-disable-next-line @typescript-eslint/no-empty-object-type
35
53
  export interface SRSConfig {
36
- // Future: configurable urgency curves, thresholds, etc.
54
+ /**
55
+ * Target "healthy" backlog size.
56
+ * When due reviews exceed this, urgency increases globally.
57
+ * Default: 20
58
+ */
59
+ healthyBacklog?: number;
37
60
  }
38
61
 
39
62
  /**
@@ -45,6 +68,7 @@ export interface SRSConfig {
45
68
  * Higher scores indicate more urgent reviews:
46
69
  * - Cards that are more overdue (relative to their interval) score higher
47
70
  * - Cards with shorter intervals (recent learning) score higher
71
+ * - When backlog exceeds "healthy" threshold, all reviews get urgency boost
48
72
  *
49
73
  * Only returns cards that are actually due (reviewTime has passed).
50
74
  * Does not generate new cards - use with CompositeGenerator for mixed content.
@@ -53,6 +77,9 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
53
77
  /** Human-readable name for CardGenerator interface */
54
78
  name: string;
55
79
 
80
+ /** Healthy backlog threshold - when exceeded, backlog pressure kicks in */
81
+ private healthyBacklog: number;
82
+
56
83
  constructor(
57
84
  user: UserDBInterface,
58
85
  course: CourseDBInterface,
@@ -60,6 +87,23 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
60
87
  ) {
61
88
  super(user, course, strategyData as ContentNavigationStrategyData);
62
89
  this.name = strategyData?.name || 'SRS';
90
+
91
+ // Parse config from serializedData if available
92
+ const config = this.parseConfig(strategyData?.serializedData);
93
+ this.healthyBacklog = config.healthyBacklog ?? DEFAULT_HEALTHY_BACKLOG;
94
+ }
95
+
96
+ /**
97
+ * Parse configuration from serialized JSON.
98
+ */
99
+ private parseConfig(serializedData?: string): SRSConfig {
100
+ if (!serializedData) return {};
101
+ try {
102
+ return JSON.parse(serializedData) as SRSConfig;
103
+ } catch {
104
+ logger.warn('[SRS] Failed to parse strategy config, using defaults');
105
+ return {};
106
+ }
63
107
  }
64
108
 
65
109
  /**
@@ -68,6 +112,7 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
68
112
  * Score formula combines:
69
113
  * - Relative overdueness: hoursOverdue / intervalHours
70
114
  * - Interval recency: exponential decay favoring shorter intervals
115
+ * - Backlog pressure: boost when due reviews exceed healthy threshold
71
116
  *
72
117
  * Cards not yet due are excluded (not scored as 0).
73
118
  *
@@ -82,14 +127,48 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
82
127
  throw new Error('SRSNavigator requires user and course to be set');
83
128
  }
84
129
 
85
- const reviews = await this.user.getPendingReviews(this.course.getCourseID());
130
+ const courseId = this.course.getCourseID();
131
+ const reviews = await this.user.getPendingReviews(courseId);
86
132
  const now = moment.utc();
87
133
 
88
134
  // Filter to only cards that are actually due
89
135
  const dueReviews = reviews.filter((r) => now.isAfter(moment.utc(r.reviewTime)));
90
136
 
137
+ // Compute backlog pressure - applies globally to all reviews
138
+ const backlogPressure = this.computeBacklogPressure(dueReviews.length);
139
+
140
+ // Log review status for transparency
141
+ if (dueReviews.length > 0) {
142
+ const pressureNote =
143
+ backlogPressure > 0
144
+ ? ` [backlog pressure: +${backlogPressure.toFixed(2)}]`
145
+ : ` [healthy backlog]`;
146
+ logger.info(
147
+ `[SRS] Course ${courseId}: ${dueReviews.length} reviews due now (of ${reviews.length} scheduled)${pressureNote}`
148
+ );
149
+ } else if (reviews.length > 0) {
150
+ // Reviews exist but none are due yet - show when next one is due
151
+ const sortedByDue = [...reviews].sort((a, b) =>
152
+ moment.utc(a.reviewTime).diff(moment.utc(b.reviewTime))
153
+ );
154
+ const nextDue = sortedByDue[0];
155
+ const nextDueTime = moment.utc(nextDue.reviewTime);
156
+ const untilDue = moment.duration(nextDueTime.diff(now));
157
+ const untilDueStr =
158
+ untilDue.asHours() < 1
159
+ ? `${Math.round(untilDue.asMinutes())}m`
160
+ : untilDue.asHours() < 24
161
+ ? `${Math.round(untilDue.asHours())}h`
162
+ : `${Math.round(untilDue.asDays())}d`;
163
+ logger.info(
164
+ `[SRS] Course ${courseId}: 0 reviews due now (${reviews.length} scheduled, next in ${untilDueStr})`
165
+ );
166
+ } else {
167
+ logger.info(`[SRS] Course ${courseId}: No reviews scheduled`);
168
+ }
169
+
91
170
  const scored = dueReviews.map((review) => {
92
- const { score, reason } = this.computeUrgencyScore(review, now);
171
+ const { score, reason } = this.computeUrgencyScore(review, now, backlogPressure);
93
172
 
94
173
  return {
95
174
  cardId: review.cardId,
@@ -109,16 +188,41 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
109
188
  };
110
189
  });
111
190
 
112
- logger.debug(`[srsNav] got ${scored.length} weighted cards`);
113
-
114
191
  // Sort by score descending and limit
115
192
  return scored.sort((a, b) => b.score - a.score).slice(0, limit);
116
193
  }
117
194
 
195
+ /**
196
+ * Compute backlog pressure based on number of due reviews.
197
+ *
198
+ * Backlog pressure is 0 when at or below healthy threshold,
199
+ * and increases linearly above it, maxing out at MAX_BACKLOG_PRESSURE.
200
+ *
201
+ * Examples (with default healthyBacklog=20):
202
+ * - 10 due reviews → 0.00 (healthy)
203
+ * - 20 due reviews → 0.00 (at threshold)
204
+ * - 40 due reviews → 0.25 (2x threshold)
205
+ * - 60 due reviews → 0.50 (3x threshold, maxed)
206
+ *
207
+ * @param dueCount - Number of reviews currently due
208
+ * @returns Backlog pressure score to add to urgency (0 to MAX_BACKLOG_PRESSURE)
209
+ */
210
+ private computeBacklogPressure(dueCount: number): number {
211
+ if (dueCount <= this.healthyBacklog) {
212
+ return 0;
213
+ }
214
+
215
+ // Linear increase: at 2x healthy, pressure = 0.25; at 3x, pressure = 0.50
216
+ const excess = dueCount - this.healthyBacklog;
217
+ const pressure = (excess / this.healthyBacklog) * (MAX_BACKLOG_PRESSURE / 2);
218
+
219
+ return Math.min(MAX_BACKLOG_PRESSURE, pressure);
220
+ }
221
+
118
222
  /**
119
223
  * Compute urgency score for a review card.
120
224
  *
121
- * Two factors:
225
+ * Three factors:
122
226
  * 1. Relative overdueness = hoursOverdue / intervalHours
123
227
  * - 2 days overdue on 3-day interval = 0.67 (urgent)
124
228
  * - 2 days overdue on 180-day interval = 0.01 (not urgent)
@@ -128,12 +232,22 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
128
232
  * - 30 days (720h) → ~0.56
129
233
  * - 180 days → ~0.30
130
234
  *
131
- * Combined: base 0.5 + weighted average of factors * 0.45
132
- * Result range: approximately 0.5 to 0.95
235
+ * 3. Backlog pressure = global boost when review backlog exceeds healthy threshold
236
+ * - At healthy backlog: 0
237
+ * - At 2x healthy: +0.25
238
+ * - At 3x+ healthy: +0.50 (max)
239
+ *
240
+ * Combined: base 0.5 + (urgency factors * 0.45) + backlog pressure
241
+ * Result range: 0.5 to 1.0 (uncapped to allow high-urgency reviews to compete with new cards)
242
+ *
243
+ * @param review - The scheduled card to score
244
+ * @param now - Current time
245
+ * @param backlogPressure - Pre-computed backlog pressure (0 to 0.5)
133
246
  */
134
247
  private computeUrgencyScore(
135
248
  review: ScheduledCard,
136
- now: moment.Moment
249
+ now: moment.Moment,
250
+ backlogPressure: number
137
251
  ): { score: number; reason: string } {
138
252
  const scheduledAt = moment.utc(review.scheduledAt);
139
253
  const due = moment.utc(review.reviewTime);
@@ -154,13 +268,27 @@ export default class SRSNavigator extends ContentNavigator implements CardGenera
154
268
  const overdueContribution = Math.min(1.0, Math.max(0, relativeOverdue));
155
269
  const urgency = overdueContribution * 0.5 + recencyFactor * 0.5;
156
270
 
157
- // Final score: base 0.5 + urgency contribution, capped at 0.95
158
- const score = Math.min(0.95, 0.5 + urgency * 0.45);
271
+ // Final score: base 0.5 + urgency contribution + backlog pressure
272
+ // Uncapped at 1.0 (no 0.95 ceiling) - allows high-urgency reviews to compete with new cards
273
+ const baseScore = 0.5 + urgency * 0.45;
274
+ const score = Math.min(1.0, baseScore + backlogPressure);
159
275
 
160
- const reason =
161
- `${Math.round(hoursOverdue)}h overdue (interval: ${Math.round(intervalHours)}h, ` +
162
- `relative: ${relativeOverdue.toFixed(2)}), recency: ${recencyFactor.toFixed(2)}, review`;
276
+ // Build reason string with all contributing factors
277
+ const reasonParts = [
278
+ `${Math.round(hoursOverdue)}h overdue`,
279
+ `interval: ${Math.round(intervalHours)}h`,
280
+ `relative: ${relativeOverdue.toFixed(2)}`,
281
+ `recency: ${recencyFactor.toFixed(2)}`,
282
+ ];
283
+
284
+ if (backlogPressure > 0) {
285
+ reasonParts.push(`backlog: +${backlogPressure.toFixed(2)}`);
286
+ }
287
+
288
+ reasonParts.push('review');
289
+
290
+ const reason = reasonParts.join(', ');
163
291
 
164
292
  return { score, reason };
165
293
  }
166
- }
294
+ }
@@ -6,6 +6,15 @@ export type { CardFilter, FilterContext, CardFilterFactory } from './filters/typ
6
6
  // Re-export generator types
7
7
  export type { CardGenerator, GeneratorContext, CardGeneratorFactory } from './generators/types';
8
8
 
9
+ // Re-export pipeline debugger API
10
+ export {
11
+ pipelineDebugAPI,
12
+ mountPipelineDebugger,
13
+ type PipelineRunReport,
14
+ type GeneratorSummary,
15
+ type FilterImpact,
16
+ } from './PipelineDebugger';
17
+
9
18
  import { LearnableWeight } from '../types/contentNavigationStrategy';
10
19
  export type { ContentNavigationStrategyData, LearnableWeight } from '../types/contentNavigationStrategy';
11
20
  import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
@@ -52,6 +52,18 @@ export class UsrCrsData implements UsrCrsDataInterface {
52
52
  }
53
53
  }
54
54
 
55
+ public async getStrategyState<T>(strategyKey: string): Promise<T | null> {
56
+ return this.user.getStrategyState<T>(this._courseId, strategyKey);
57
+ }
58
+
59
+ public async putStrategyState<T>(strategyKey: string, data: T): Promise<void> {
60
+ return this.user.putStrategyState<T>(this._courseId, strategyKey, data);
61
+ }
62
+
63
+ public async deleteStrategyState(strategyKey: string): Promise<void> {
64
+ return this.user.deleteStrategyState(this._courseId, strategyKey);
65
+ }
66
+
55
67
  private async getReviewstoDate(targetDate: Moment) {
56
68
  // Use the interface method instead of direct database access
57
69
  const allReviews = await this.user.getPendingReviews(this._courseId);