@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
@@ -46,11 +46,28 @@ interface CardGenerator {
46
46
  ```
47
47
 
48
48
  **Implementations:**
49
- - `ELONavigator` — New cards scored by ELO proximity to user skill
50
- - `SRSNavigator` — Review cards scored by overdueness and interval recency
49
+ - `ELONavigator` — New cards scored by ELO proximity to user skill (scores 0.0-1.0)
50
+ - `SRSNavigator` — Review cards scored by overdueness, interval recency, and **backlog pressure** (scores 0.5-1.0)
51
51
  - `HardcodedOrderNavigator` — Fixed sequence defined by course author
52
52
  - `CompositeGenerator` — Merges multiple generators with frequency boost
53
53
 
54
+ #### SRS Backlog Pressure
55
+
56
+ The SRS generator implements a self-regulating **backlog pressure** mechanism that prevents review pile-up while maintaining healthy new/review balance:
57
+
58
+ - **Healthy backlog** (≤20 due reviews): No pressure boost, scores 0.5-0.95. New content (ELO) naturally dominates.
59
+ - **Elevated backlog** (40 due): +0.25 boost, scores 0.75-1.0. Reviews compete with new cards.
60
+ - **High backlog** (60+ due): +0.50 boost (max), scores 0.95-1.0. Reviews take priority.
61
+
62
+ This treats SRS scheduling times as **eligibility dates** rather than hard due dates—reviewing slightly later may be optimal. The system maintains a healthy backlog rather than always clearing to zero (avoiding "Anki death spiral").
63
+
64
+ Configuration via strategy `serializedData`:
65
+ ```json
66
+ { "healthyBacklog": 20 }
67
+ ```
68
+
69
+ See `todo-review-adaptation.md` for planned per-user adaptation extensions.
70
+
54
71
  ### CardFilter
55
72
 
56
73
  Transforms card scores (pure function, no side effects):
@@ -516,8 +533,8 @@ return { ...card, score: card.score * multiplier };
516
533
  | `core/navigators/Pipeline.ts` | Pipeline orchestration |
517
534
  | `core/navigators/PipelineAssembler.ts` | Builds Pipeline from strategy docs |
518
535
  | `core/navigators/CompositeGenerator.ts` | Merges multiple generators |
519
- | `core/navigators/elo.ts` | ELO generator |
520
- | `core/navigators/srs.ts` | SRS generator |
536
+ | `core/navigators/generators/elo.ts` | ELO generator |
537
+ | `core/navigators/generators/srs.ts` | SRS generator (with backlog pressure) |
521
538
  | `core/navigators/hardcodedOrder.ts` | Fixed-order generator |
522
539
  | `core/navigators/hierarchyDefinition.ts` | Prerequisite filter |
523
540
  | `core/navigators/interferenceMitigator.ts` | Interference filter |
@@ -541,6 +558,7 @@ return { ...card, score: card.score * multiplier };
541
558
  ## Related Documentation
542
559
 
543
560
  - `todo-strategy-authoring.md` — UX and DX for authoring strategies
561
+ - `todo-review-adaptation.md` — Planned per-user review urgency adaptation
544
562
  - `future-orchestration-vision.md` — Long-term adaptive strategy vision (beyond current implementation)
545
563
  - `devlog/1004` — Implementation details for tag hydration optimization
546
564
  - `devlog/1032-orchestrator` — Evolutionary orchestration implementation details
@@ -0,0 +1,205 @@
1
+ # Future Work: Dynamic Review Urgency Adaptation
2
+
3
+ This document outlines planned extensions to the SRS backlog pressure system for adaptive review urgency tuning.
4
+
5
+ ## Background
6
+
7
+ The SRS generator now implements **backlog pressure** — a mechanism that boosts review urgency when the number of due reviews exceeds a "healthy" threshold (default: 20). This ensures reviews don't pile up indefinitely while maintaining a healthy mix of new and review content.
8
+
9
+ See implementation: `core/navigators/generators/srs.ts`
10
+
11
+ This document describes two extension directions:
12
+ 1. **Global/Orchestration Layer** — Course-wide tuning via learnable weights
13
+ 2. **Per-User Adaptation** — Individual urgency multipliers based on review outcomes
14
+
15
+ ---
16
+
17
+ ## Extension 1: Orchestrator-Tuned Generator Weights
18
+
19
+ ### Current State
20
+
21
+ The orchestration layer (`core/orchestration/`) already supports learnable weights:
22
+
23
+ ```typescript
24
+ interface LearnableWeight {
25
+ weight: number; // Peak value, 1.0 = neutral
26
+ confidence: number; // 0-1, controls exploration width
27
+ sampleSize: number; // Observations collected
28
+ }
29
+ ```
30
+
31
+ When a strategy has `learnable` set and `staticWeight: false`, the orchestrator:
32
+ 1. Distributes users across a weight range (bell curve around peak)
33
+ 2. Records learning outcomes per user
34
+ 3. Correlates user deviation with outcomes via gradient estimation
35
+ 4. Updates peak weight toward observed optimum
36
+
37
+ ### Application to Review Balance
38
+
39
+ The orchestrator can tune the ELO vs SRS balance by experimenting with their weights.
40
+
41
+ In `CompositeGenerator.getWeightedCards()`, weights are already applied:
42
+
43
+ ```typescript
44
+ let weight = gen.learnable?.weight ?? 1.0;
45
+ if (gen.learnable && !gen.staticWeight && context.orchestration) {
46
+ weight = context.orchestration.getEffectiveWeight(strategyId, gen.learnable);
47
+ }
48
+ ```
49
+
50
+ If SRS has `learnable: { weight: 1.0, confidence: 0.5, sampleSize: 0 }`, the orchestrator will try variations (0.8, 1.2, etc.) and correlate with learning outcomes.
51
+
52
+ ### Required Work
53
+
54
+ - [ ] Ensure default ELO and SRS strategy documents include `learnable` config
55
+ - [ ] Verify outcome correlation tracks success/failure per card source (generator provenance)
56
+ - [ ] Consider weight bounds (e.g., SRS weight ∈ [0.5, 2.0]) to prevent extreme tuning
57
+ - [ ] Test interaction with backlog pressure — may need to mark backlog-pressure-derived scores as non-learnable
58
+
59
+ ### Risks
60
+
61
+ - Orchestrator experiments could override backlog pressure intent
62
+ - May need to separate "base urgency" (learnable) from "backlog pressure" (not learnable)
63
+
64
+ ### Priority
65
+
66
+ **Low** — The backlog pressure mechanism should handle balance well. Orchestrator tuning is a refinement layer that can be enabled later.
67
+
68
+ ---
69
+
70
+ ## Extension 2: Per-User Review Urgency Adaptation
71
+
72
+ ### Concept
73
+
74
+ Users have different memory characteristics. A "forgetful" user needs more review pressure; a "retentive" user needs less. Rather than one-size-fits-all, maintain a per-user `reviewUrgencyMultiplier` that adapts based on review outcomes.
75
+
76
+ ### Proposed Data Model
77
+
78
+ ```typescript
79
+ interface ReviewAdaptationState {
80
+ urgencyMultiplier: number; // Default 1.0, range [0.5, 2.0]
81
+ windowStart: string; // ISO timestamp for rolling window
82
+ windowFailures: number; // Failures in current window
83
+ windowSuccesses: number; // Successes in current window
84
+ lastUpdated: string; // ISO timestamp
85
+ }
86
+ ```
87
+
88
+ ### Update Rules
89
+
90
+ - On failed review: `multiplier = min(2.0, multiplier + 0.05)`
91
+ - On successful review: `multiplier = max(0.5, multiplier - 0.01)`
92
+
93
+ **Asymmetry rationale**:
94
+ - Failures increase pressure quickly (5x faster than decrease)
95
+ - Successes slowly reduce pressure (avoid death spiral from one good session)
96
+
97
+ ### Equilibrium Analysis
98
+
99
+ At equilibrium: `0.05 * failRate = 0.01 * (1 - failRate)`
100
+
101
+ Solving: `failRate = 0.167` (16.7%)
102
+
103
+ This means:
104
+ - User with 16.7% failure rate → multiplier stable
105
+ - User with >16.7% failure rate → multiplier increases (more review pressure)
106
+ - User with <16.7% failure rate → multiplier decreases (less review pressure)
107
+
108
+ The equilibrium threshold can be tuned by adjusting increment/decrement rates.
109
+
110
+ ### Application in Scoring
111
+
112
+ In SRS urgency calculation:
113
+
114
+ ```typescript
115
+ const userAdaptation = await this.getReviewAdaptation();
116
+ const adaptedUrgency = baseUrgency * userAdaptation.urgencyMultiplier;
117
+ const score = Math.min(1.0, 0.5 + adaptedUrgency * 0.45 + backlogPressure);
118
+ ```
119
+
120
+ Note: multiplier affects base urgency, not backlog pressure. This preserves the global backlog escape mechanism.
121
+
122
+ ### Storage
123
+
124
+ Use strategy state storage (per-user, per-course, per-strategy):
125
+
126
+ ```typescript
127
+ const state = await this.getStrategyState<ReviewAdaptationState>();
128
+ await this.putStrategyState({ ...state, urgencyMultiplier: newValue });
129
+ ```
130
+
131
+ ### Implementation Sketch
132
+
133
+ ```typescript
134
+ // In srs.ts
135
+
136
+ private async getReviewAdaptation(): Promise<ReviewAdaptationState> {
137
+ const state = await this.getStrategyState<ReviewAdaptationState>();
138
+ return state || {
139
+ urgencyMultiplier: 1.0,
140
+ windowStart: new Date().toISOString(),
141
+ windowFailures: 0,
142
+ windowSuccesses: 0,
143
+ lastUpdated: new Date().toISOString(),
144
+ };
145
+ }
146
+
147
+ // Called after review outcome recorded (needs integration hook)
148
+ async updateReviewAdaptation(success: boolean): Promise<void> {
149
+ const state = await this.getReviewAdaptation();
150
+
151
+ if (success) {
152
+ state.urgencyMultiplier = Math.max(0.5, state.urgencyMultiplier - 0.01);
153
+ state.windowSuccesses++;
154
+ } else {
155
+ state.urgencyMultiplier = Math.min(2.0, state.urgencyMultiplier + 0.05);
156
+ state.windowFailures++;
157
+ }
158
+
159
+ state.lastUpdated = new Date().toISOString();
160
+ await this.putStrategyState(state);
161
+ }
162
+ ```
163
+
164
+ ### Required Work
165
+
166
+ - [ ] Add `getReviewAdaptation()` method to SRSNavigator
167
+ - [ ] Integrate `updateReviewAdaptation()` with card response recording flow
168
+ - [ ] Decide: Should multiplier affect backlog pressure or just base urgency?
169
+ - [ ] Consider reset behavior on long breaks (user returning after 6 months)
170
+ - [ ] Consider course-specific vs global multipliers
171
+ - [ ] Add provenance entry explaining multiplier contribution
172
+
173
+ ### Open Questions
174
+
175
+ 1. **Who calls `updateReviewAdaptation`?** — Needs hook into card response recording (currently in `UserDB.recordInteraction()` or similar)
176
+
177
+ 2. **Rolling window reset?** — Should the window reset periodically to allow recovery from bad streaks?
178
+
179
+ 3. **Visibility?** — Should users see their current multiplier? Could be motivating or discouraging.
180
+
181
+ 4. **Initial calibration?** — New users start at 1.0. Should there be an initial calibration period with more aggressive updates?
182
+
183
+ ### Priority
184
+
185
+ **Medium** — High value for learning outcomes, moderate implementation complexity. Requires integration with card response recording flow.
186
+
187
+ ---
188
+
189
+ ## Implementation Order
190
+
191
+ 1. ✅ **Backlog Pressure** (implemented) — Fixes immediate balance issue
192
+ 2. **Per-User Adaptation** — High value, moderate complexity
193
+ 3. **Orchestrator Tuning** — Lower priority if backlog pressure works well
194
+
195
+ ---
196
+
197
+ ## Related Files
198
+
199
+ | File | Purpose |
200
+ |------|---------|
201
+ | `core/navigators/generators/srs.ts` | SRS urgency calculation, backlog pressure |
202
+ | `core/orchestration/index.ts` | Orchestration context, deviation logic |
203
+ | `core/types/contentNavigationStrategy.ts` | Strategy config, learnable weights |
204
+ | `core/types/strategyState.ts` | Per-user strategy state storage |
205
+ | `impl/couch/userDB.ts` | User interaction recording |
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.1.24",
7
+ "version": "0.1.25",
8
8
  "description": "Database layer for vue-skuilder",
9
9
  "main": "dist/index.js",
10
10
  "module": "dist/index.mjs",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@nilock2/pouchdb-authentication": "^1.0.2",
51
- "@vue-skuilder/common": "0.1.24",
51
+ "@vue-skuilder/common": "0.1.25",
52
52
  "cross-fetch": "^4.1.0",
53
53
  "moment": "^2.29.4",
54
54
  "pouchdb": "^9.0.0",
@@ -62,5 +62,5 @@
62
62
  "vite": "^7.0.0",
63
63
  "vitest": "^4.0.15"
64
64
  },
65
- "stableVersion": "0.1.24"
65
+ "stableVersion": "0.1.25"
66
66
  }
@@ -63,6 +63,11 @@ export interface UserDBReader {
63
63
  * Strategies use this to persist preferences, learned patterns, or temporal
64
64
  * tracking data across sessions. Each strategy owns its own namespace.
65
65
  *
66
+ * @deprecated Use `getCourseInterface(courseId).getStrategyState(strategyKey)` instead.
67
+ * Direct use bypasses course-scoping safety — the courseId parameter is unguarded,
68
+ * allowing accidental cross-course data access. The course-scoped interface binds
69
+ * courseId once at construction.
70
+ *
66
71
  * @param courseId - The course this state applies to
67
72
  * @param strategyKey - Unique key identifying the strategy (typically class name)
68
73
  * @returns The strategy's data payload, or null if no state exists
@@ -152,6 +157,11 @@ export interface UserDBWriter extends DocumentUpdater {
152
157
  * Strategies use this to persist preferences, learned patterns, or temporal
153
158
  * tracking data across sessions. Each strategy owns its own namespace.
154
159
  *
160
+ * @deprecated Use `getCourseInterface(courseId).putStrategyState(strategyKey, data)` instead.
161
+ * Direct use bypasses course-scoping safety — the courseId parameter is unguarded,
162
+ * allowing accidental cross-course data writes. The course-scoped interface binds
163
+ * courseId once at construction.
164
+ *
155
165
  * @param courseId - The course this state applies to
156
166
  * @param strategyKey - Unique key identifying the strategy (typically class name)
157
167
  * @param data - The strategy's data payload to store
@@ -161,6 +171,9 @@ export interface UserDBWriter extends DocumentUpdater {
161
171
  /**
162
172
  * Delete strategy-specific state for a course.
163
173
  *
174
+ * @deprecated Use `getCourseInterface(courseId).deleteStrategyState(strategyKey)` instead.
175
+ * Direct use bypasses course-scoping safety.
176
+ *
164
177
  * @param courseId - The course this state applies to
165
178
  * @param strategyKey - Unique key identifying the strategy (typically class name)
166
179
  */
@@ -228,6 +241,37 @@ export interface UsrCrsDataInterface {
228
241
  getCourseSettings(): Promise<UserCourseSettings>;
229
242
  updateCourseSettings(updates: UserCourseSetting[]): void; // [ ] return a result of some sort?
230
243
  // getRegistrationDoc(): Promise<CourseRegistration>;
244
+
245
+ /**
246
+ * Get strategy-specific state for this course.
247
+ *
248
+ * Course-scoped alternative to `UserDBInterface.getStrategyState()`.
249
+ * The courseId is bound at construction via `getCourseInterface(courseId)`,
250
+ * so callers cannot accidentally access another course's state.
251
+ *
252
+ * @param strategyKey - Unique key identifying the state document
253
+ * @returns The state payload, or null if no state exists
254
+ */
255
+ getStrategyState<T>(strategyKey: string): Promise<T | null>;
256
+
257
+ /**
258
+ * Store strategy-specific state for this course.
259
+ *
260
+ * Course-scoped alternative to `UserDBInterface.putStrategyState()`.
261
+ *
262
+ * @param strategyKey - Unique key identifying the state document
263
+ * @param data - The state payload to store
264
+ */
265
+ putStrategyState<T>(strategyKey: string, data: T): Promise<void>;
266
+
267
+ /**
268
+ * Delete strategy-specific state for this course.
269
+ *
270
+ * Course-scoped alternative to `UserDBInterface.deleteStrategyState()`.
271
+ *
272
+ * @param strategyKey - Unique key identifying the state document
273
+ */
274
+ deleteStrategyState(strategyKey: string): Promise<void>;
231
275
  }
232
276
 
233
277
  export type ClassroomRegistrationDesignation = 'student' | 'teacher' | 'aide' | 'admin';
@@ -7,6 +7,7 @@ import type { CardFilter, FilterContext } from './filters/types';
7
7
  import type { CardGenerator, GeneratorContext } from './generators/types';
8
8
  import { logger } from '../../util/logger';
9
9
  import { createOrchestrationContext, OrchestrationContext } from '../orchestration';
10
+ import { captureRun, buildRunReport, type GeneratorSummary, type FilterImpact } from './PipelineDebugger';
10
11
 
11
12
  // ============================================================================
12
13
  // PIPELINE LOGGING HELPERS
@@ -52,14 +53,29 @@ function logExecutionSummary(
52
53
  generatedCount: number,
53
54
  filterCount: number,
54
55
  finalCount: number,
55
- topScores: number[]
56
+ topScores: number[],
57
+ filterImpacts: Array<{ name: string; boosted: number; penalized: number; passed: number }>
56
58
  ): void {
57
59
  const scoreDisplay =
58
60
  topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(', ') : 'none';
59
61
 
62
+ let filterSummary = '';
63
+ if (filterImpacts.length > 0) {
64
+ const impacts = filterImpacts.map((f) => {
65
+ const parts: string[] = [];
66
+ if (f.boosted > 0) parts.push(`+${f.boosted}`);
67
+ if (f.penalized > 0) parts.push(`-${f.penalized}`);
68
+ if (f.passed > 0) parts.push(`=${f.passed}`);
69
+ return `${f.name}: ${parts.join('/')}`;
70
+ });
71
+ filterSummary = `\n Filter impact: ${impacts.join(', ')}`;
72
+ }
73
+
60
74
  logger.info(
61
75
  `[Pipeline] Execution: ${generatorName} produced ${generatedCount} → ` +
62
- `${filterCount} filters → ${finalCount} results (top scores: ${scoreDisplay})`
76
+ `${filterCount} filters → ${finalCount} results (top scores: ${scoreDisplay})` +
77
+ filterSummary +
78
+ `\n 💡 Inspect: window.skuilder.pipeline`
63
79
  );
64
80
  }
65
81
 
@@ -190,17 +206,63 @@ export class Pipeline extends ContentNavigator {
190
206
  // Get candidates from generator, passing context
191
207
  let cards = await this.generator.getWeightedCards(fetchLimit, context);
192
208
  const generatedCount = cards.length;
209
+
210
+ // Capture generator breakdown for debugging (if CompositeGenerator)
211
+ let generatorSummaries: GeneratorSummary[] | undefined;
212
+ if ((this.generator as any).generators) {
213
+ // This is a CompositeGenerator - extract per-generator info from provenance
214
+ const genMap = new Map<string, { cards: WeightedCard[] }>();
215
+ for (const card of cards) {
216
+ const firstProv = card.provenance[0];
217
+ if (firstProv) {
218
+ const genName = firstProv.strategyName;
219
+ if (!genMap.has(genName)) {
220
+ genMap.set(genName, { cards: [] });
221
+ }
222
+ genMap.get(genName)!.cards.push(card);
223
+ }
224
+ }
225
+ generatorSummaries = Array.from(genMap.entries()).map(([name, data]) => {
226
+ const newCards = data.cards.filter((c) => c.provenance[0]?.reason?.includes('new card'));
227
+ const reviewCards = data.cards.filter((c) => c.provenance[0]?.reason?.includes('review'));
228
+ return {
229
+ name,
230
+ cardCount: data.cards.length,
231
+ newCount: newCards.length,
232
+ reviewCount: reviewCards.length,
233
+ topScore: Math.max(...data.cards.map((c) => c.score), 0),
234
+ };
235
+ });
236
+ }
193
237
 
194
238
  logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
195
239
 
196
240
  // Batch hydrate tags before filters run
197
241
  cards = await this.hydrateTags(cards);
242
+
243
+ // Keep a copy of all cards for debug capture (before filtering removes any)
244
+ const allCardsBeforeFiltering = [...cards];
198
245
 
199
- // Apply filters sequentially
246
+ // Apply filters sequentially, tracking impact
247
+ const filterImpacts: FilterImpact[] = [];
200
248
  for (const filter of this.filters) {
201
249
  const beforeCount = cards.length;
250
+ const beforeScores = new Map(cards.map((c) => [c.cardId, c.score]));
202
251
  cards = await filter.transform(cards, context);
203
- logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeCount} → ${cards.length} cards`);
252
+
253
+ // Count boost/penalize/pass/removed for this filter
254
+ let boosted = 0, penalized = 0, passed = 0;
255
+ const removed = beforeCount - cards.length;
256
+
257
+ for (const card of cards) {
258
+ const before = beforeScores.get(card.cardId) ?? 0;
259
+ if (card.score > before) boosted++;
260
+ else if (card.score < before) penalized++;
261
+ else passed++;
262
+ }
263
+ filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
264
+
265
+ logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} → ${cards.length} cards (↑${boosted} ↓${penalized} =${passed})`);
204
266
  }
205
267
 
206
268
  // Remove zero-score cards (hard filtered)
@@ -219,12 +281,31 @@ export class Pipeline extends ContentNavigator {
219
281
  generatedCount,
220
282
  this.filters.length,
221
283
  result.length,
222
- topScores
284
+ topScores,
285
+ filterImpacts
223
286
  );
224
287
 
225
288
  // Toggle provenance logging (shows scoring history for top cards):
226
289
  logCardProvenance(result, 3);
227
290
 
291
+ // Capture run for debug API
292
+ try {
293
+ const courseName = await this.course?.getCourseConfig().then((c) => c.name).catch(() => undefined);
294
+ const report = buildRunReport(
295
+ this.course?.getCourseID() || 'unknown',
296
+ courseName,
297
+ this.generator.name,
298
+ generatorSummaries,
299
+ generatedCount,
300
+ filterImpacts,
301
+ allCardsBeforeFiltering,
302
+ result
303
+ );
304
+ captureRun(report);
305
+ } catch (e) {
306
+ logger.debug(`[Pipeline] Failed to capture debug run: ${e}`);
307
+ }
308
+
228
309
  return result;
229
310
  }
230
311
 
@@ -1,14 +1,14 @@
1
1
  import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
2
- import { ContentNavigator, isGenerator, isFilter, Navigators } from './index';
2
+ import { ContentNavigator, isGenerator, isFilter } from './index';
3
3
  import type { CardFilter } from './filters/types';
4
4
  import { WeightedFilter } from './filters/WeightedFilter';
5
5
  import type { CardGenerator } from './generators/types';
6
6
  import { Pipeline } from './Pipeline';
7
- import { DocType } from '../types/types-legacy';
8
7
  import { logger } from '../../util/logger';
9
8
  import type { CourseDBInterface } from '../interfaces/courseDB';
10
9
  import type { UserDBInterface } from '../interfaces/userDB';
11
10
  import CompositeGenerator from './generators/CompositeGenerator';
11
+ import { createDefaultEloStrategy, createDefaultSrsStrategy } from './defaults';
12
12
 
13
13
  // ============================================================================
14
14
  // PIPELINE ASSEMBLER
@@ -103,13 +103,15 @@ export class PipelineAssembler {
103
103
  }
104
104
  }
105
105
 
106
- // If no generator but filters exist, use default ELO generator
106
+ // If no generator but filters exist, use default ELO and SRS generators
107
107
  if (generatorStrategies.length === 0) {
108
108
  if (filterStrategies.length > 0) {
109
109
  logger.debug(
110
- '[PipelineAssembler] No generator found, using default ELO with configured filters'
110
+ '[PipelineAssembler] No generator found, using default ELO and SRS with configured filters'
111
111
  );
112
- generatorStrategies.push(this.makeDefaultEloStrategy(course.getCourseID()));
112
+ const courseId = course.getCourseID();
113
+ generatorStrategies.push(createDefaultEloStrategy(courseId));
114
+ generatorStrategies.push(createDefaultSrsStrategy(courseId));
113
115
  } else {
114
116
  warnings.push('No generator strategy found');
115
117
  return {
@@ -188,20 +190,4 @@ export class PipelineAssembler {
188
190
  warnings,
189
191
  };
190
192
  }
191
-
192
- /**
193
- * Creates a default ELO generator strategy.
194
- * Used when filters are configured but no generator is specified.
195
- */
196
- private makeDefaultEloStrategy(courseId: string): ContentNavigationStrategyData {
197
- return {
198
- _id: 'NAVIGATION_STRATEGY-ELO-default',
199
- course: courseId,
200
- docType: DocType.NAVIGATION_STRATEGY,
201
- name: 'ELO (default)',
202
- description: 'Default ELO-based generator',
203
- implementingClass: Navigators.ELO,
204
- serializedData: '',
205
- };
206
- }
207
193
  }