@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
|
@@ -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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
}
|