@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.
- package/dist/{contentSource-BotbOOfX.d.ts → contentSource-BmnmvH8C.d.ts} +41 -0
- package/dist/{contentSource-C90LH-OH.d.cts → contentSource-DfBbaLA-.d.cts} +41 -0
- package/dist/core/index.d.cts +94 -4
- package/dist/core/index.d.ts +94 -4
- package/dist/core/index.js +530 -83
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +528 -83
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-DGKp4zFB.d.cts → dataLayerProvider-BeRXVMs5.d.cts} +1 -1
- package/dist/{dataLayerProvider-SBpz9jQf.d.ts → dataLayerProvider-CG9GfaAY.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +2 -2
- package/dist/impl/couch/index.d.ts +2 -2
- package/dist/impl/couch/index.js +526 -83
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +526 -83
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +2 -2
- package/dist/impl/static/index.d.ts +2 -2
- package/dist/impl/static/index.js +526 -83
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +526 -83
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +247 -14
- package/dist/index.d.ts +247 -14
- package/dist/index.js +1419 -140
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1409 -137
- package/dist/index.mjs.map +1 -1
- package/docs/navigators-architecture.md +22 -4
- package/docs/todo-review-urgency-adaptation.md +205 -0
- package/package.json +3 -3
- package/src/core/interfaces/userDB.ts +44 -0
- package/src/core/navigators/Pipeline.ts +86 -5
- package/src/core/navigators/PipelineAssembler.ts +7 -21
- package/src/core/navigators/PipelineDebugger.ts +426 -0
- package/src/core/navigators/generators/CompositeGenerator.ts +21 -0
- package/src/core/navigators/generators/elo.ts +14 -1
- package/src/core/navigators/generators/srs.ts +146 -18
- package/src/core/navigators/index.ts +9 -0
- package/src/impl/couch/user-course-relDB.ts +12 -0
- package/src/study/MixerDebugger.ts +555 -0
- package/src/study/SessionController.ts +95 -19
- package/src/study/SessionDebugger.ts +442 -0
- package/src/study/SourceMixer.ts +36 -17
- package/src/study/TODO-session-scheduling.md +133 -0
- package/src/study/index.ts +2 -0
- package/src/study/services/EloService.ts +79 -4
- package/src/study/services/ResponseProcessor.ts +130 -72
- package/src/study/services/SrsService.ts +9 -0
- 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
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
132
|
-
*
|
|
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
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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);
|