@vue-skuilder/db 0.1.18 → 0.1.21
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/CLAUDE.md +2 -2
- package/dist/{classroomDB-BgfrVb8d.d.ts → contentSource-BP9hznNV.d.ts} +220 -197
- package/dist/{classroomDB-CTOenngH.d.cts → contentSource-DsJadoBU.d.cts} +220 -197
- package/dist/core/index.d.cts +80 -6
- package/dist/core/index.d.ts +80 -6
- package/dist/core/index.js +735 -1560
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +708 -1539
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-D6PoCwS6.d.cts → dataLayerProvider-CHYrQ5pB.d.cts} +1 -1
- package/dist/{dataLayerProvider-CZxC9GtB.d.ts → dataLayerProvider-MDTxXq2l.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +8 -23
- package/dist/impl/couch/index.d.ts +8 -23
- package/dist/impl/couch/index.js +723 -1578
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +692 -1552
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +25 -8
- package/dist/impl/static/index.d.ts +25 -8
- package/dist/impl/static/index.js +700 -1400
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +688 -1393
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-D-Fa4Smt.d.cts → index-B_j6u5E4.d.cts} +1 -1
- package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
- package/dist/index.d.cts +71 -63
- package/dist/index.d.ts +71 -63
- package/dist/index.js +1162 -1996
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1124 -1955
- package/dist/index.mjs.map +1 -1
- package/dist/pouch/index.js +3 -0
- package/dist/pouch/index.js.map +1 -1
- package/dist/pouch/index.mjs +3 -0
- package/dist/pouch/index.mjs.map +1 -1
- package/dist/{types-CzPDLAK6.d.cts → types-Bn0itutr.d.cts} +1 -1
- package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
- package/dist/{types-legacy-6ettoclI.d.cts → types-legacy-DDY4N-Uq.d.cts} +3 -1
- package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.ts} +3 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/docs/navigators-architecture.md +115 -17
- package/package.json +4 -4
- package/src/core/index.ts +1 -0
- package/src/core/interfaces/classroomDB.ts +5 -13
- package/src/core/interfaces/contentSource.ts +6 -66
- package/src/core/interfaces/courseDB.ts +15 -7
- package/src/core/interfaces/userDB.ts +32 -0
- package/src/core/navigators/Pipeline.ts +136 -52
- package/src/core/navigators/PipelineAssembler.ts +1 -1
- package/src/core/navigators/defaults.ts +84 -0
- package/src/core/navigators/{hierarchyDefinition.ts → filters/hierarchyDefinition.ts} +15 -29
- package/src/core/navigators/filters/index.ts +3 -0
- package/src/core/navigators/filters/inferredPreferenceStub.ts +107 -0
- package/src/core/navigators/{interferenceMitigator.ts → filters/interferenceMitigator.ts} +11 -37
- package/src/core/navigators/{relativePriority.ts → filters/relativePriority.ts} +12 -38
- package/src/core/navigators/filters/userGoalStub.ts +136 -0
- package/src/core/navigators/filters/userTagPreference.ts +217 -0
- package/src/core/navigators/{CompositeGenerator.ts → generators/CompositeGenerator.ts} +15 -64
- package/src/core/navigators/{elo.ts → generators/elo.ts} +13 -63
- package/src/core/navigators/{srs.ts → generators/srs.ts} +11 -40
- package/src/core/navigators/generators/types.ts +1 -1
- package/src/core/navigators/index.ts +95 -91
- package/src/core/types/strategyState.ts +84 -0
- package/src/core/types/types-legacy.ts +2 -0
- package/src/impl/common/BaseUserDB.ts +74 -7
- package/src/impl/couch/adminDB.ts +1 -2
- package/src/impl/couch/classroomDB.ts +100 -103
- package/src/impl/couch/courseDB.ts +35 -91
- package/src/impl/couch/pouchdb-setup.ts +7 -0
- package/src/impl/static/StaticDataUnpacker.ts +50 -1
- package/src/impl/static/courseDB.ts +87 -37
- package/src/study/SessionController.ts +122 -202
- package/src/study/SourceMixer.ts +65 -0
- package/src/study/TagFilteredContentSource.ts +49 -92
- package/src/study/index.ts +1 -0
- package/src/study/services/CardHydrationService.ts +165 -81
- package/src/util/dataDirectory.ts +1 -1
- package/src/util/index.ts +0 -1
- package/tests/core/navigators/CompositeGenerator.test.ts +44 -168
- package/tests/core/navigators/Pipeline.test.ts +6 -72
- package/tests/core/navigators/PipelineAssembler.test.ts +8 -58
- package/tests/core/navigators/navigators.test.ts +118 -151
- package/docs/todo-pipeline-optimization.md +0 -117
- package/docs/todo-strategy-state-storage.md +0 -278
- package/src/core/navigators/hardcodedOrder.ts +0 -163
- package/src/util/tuiLogger.ts +0 -139
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
# TODO: Strategy-Specific State Storage in UserDB
|
|
2
|
-
|
|
3
|
-
## Status: NOT STARTED
|
|
4
|
-
|
|
5
|
-
## Goal
|
|
6
|
-
|
|
7
|
-
Enable NavigationStrategies (ContentNavigators) to persist their own state data in the
|
|
8
|
-
user's database, allowing strategies to maintain context across sessions.
|
|
9
|
-
|
|
10
|
-
## Current State
|
|
11
|
-
|
|
12
|
-
### What Strategies Can Read
|
|
13
|
-
|
|
14
|
-
| Data | Method | Notes |
|
|
15
|
-
|------|--------|-------|
|
|
16
|
-
| User's global ELO | `user.getCourseRegDoc(courseId).elo.global` | ✅ Available |
|
|
17
|
-
| User's per-tag ELO | `user.getCourseRegDoc(courseId).elo.tags` | ✅ Available |
|
|
18
|
-
| Seen cards | `user.getSeenCards(courseId)` | ✅ Card IDs only |
|
|
19
|
-
| Active cards | `user.getActiveCards()` | ✅ Available |
|
|
20
|
-
| Pending reviews | `user.getPendingReviews(courseId)` | ✅ ScheduledCard objects |
|
|
21
|
-
| Card history | `user.putCardRecord()` returns `CardHistory` | 🟡 Only after write |
|
|
22
|
-
|
|
23
|
-
### What Strategies Cannot Do
|
|
24
|
-
|
|
25
|
-
- **Store arbitrary state**: No namespaced storage for strategy-specific data
|
|
26
|
-
- **Track temporal patterns**: No easy way to record "when did I last introduce tag X?"
|
|
27
|
-
- **Persist learning context**: Strategy state is lost between sessions
|
|
28
|
-
|
|
29
|
-
## Use Cases
|
|
30
|
-
|
|
31
|
-
### 1. InterferenceMitigator
|
|
32
|
-
|
|
33
|
-
**Need**: Track when interfering concepts were last introduced together.
|
|
34
|
-
|
|
35
|
-
```typescript
|
|
36
|
-
// Desired: Store last introduction time per tag
|
|
37
|
-
{
|
|
38
|
-
"lastIntroduction": {
|
|
39
|
-
"letter-b": "2024-01-15T10:30:00Z",
|
|
40
|
-
"letter-d": "2024-01-16T14:20:00Z"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### 2. Minimal Pairs Strategy (Future)
|
|
46
|
-
|
|
47
|
-
**Need**: Track discrimination training progress between confusable pairs.
|
|
48
|
-
|
|
49
|
-
```typescript
|
|
50
|
-
// Desired: Store discrimination scores per pair
|
|
51
|
-
{
|
|
52
|
-
"pairScores": {
|
|
53
|
-
"b-d": { "correct": 15, "total": 20, "lastPracticed": "..." },
|
|
54
|
-
"m-n": { "correct": 8, "total": 10, "lastPracticed": "..." }
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### 3. Adaptive Pacing Strategy (Future)
|
|
60
|
-
|
|
61
|
-
**Need**: Track user's engagement patterns and optimal session timing.
|
|
62
|
-
|
|
63
|
-
```typescript
|
|
64
|
-
// Desired: Store engagement metrics
|
|
65
|
-
{
|
|
66
|
-
"sessionMetrics": {
|
|
67
|
-
"avgAccuracyByTimeOfDay": { "morning": 0.85, "afternoon": 0.78 },
|
|
68
|
-
"optimalSessionLength": 180, // seconds
|
|
69
|
-
"fatigueThreshold": 12 // cards before accuracy drops
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
## Proposed Solutions
|
|
75
|
-
|
|
76
|
-
### Option A: Extend CourseRegistration
|
|
77
|
-
|
|
78
|
-
Add a `strategyState` field to the existing `CourseRegistration` document.
|
|
79
|
-
|
|
80
|
-
**Schema:**
|
|
81
|
-
```typescript
|
|
82
|
-
interface CourseRegistration {
|
|
83
|
-
// ... existing fields ...
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Strategy-specific state, keyed by strategy ID or type.
|
|
87
|
-
* Each strategy owns its own namespace.
|
|
88
|
-
*/
|
|
89
|
-
strategyState?: {
|
|
90
|
-
[strategyKey: string]: unknown;
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
**Pros:**
|
|
96
|
-
- No new document types
|
|
97
|
-
- Lives alongside other course-specific user data
|
|
98
|
-
- Already synced via existing mechanisms
|
|
99
|
-
|
|
100
|
-
**Cons:**
|
|
101
|
-
- Potential for large documents if strategies store lots of data
|
|
102
|
-
- All strategies share one document (contention on updates)
|
|
103
|
-
|
|
104
|
-
---
|
|
105
|
-
|
|
106
|
-
### Option B: Separate Strategy State Documents
|
|
107
|
-
|
|
108
|
-
Create a new document type for strategy state.
|
|
109
|
-
|
|
110
|
-
**Schema:**
|
|
111
|
-
```typescript
|
|
112
|
-
interface StrategyStateDoc {
|
|
113
|
-
_id: `STRATEGY_STATE-${courseId}-${strategyKey}`;
|
|
114
|
-
docType: DocType.STRATEGY_STATE;
|
|
115
|
-
courseId: string;
|
|
116
|
-
strategyKey: string; // e.g., "interferenceMitigator" or strategy instance ID
|
|
117
|
-
data: unknown;
|
|
118
|
-
updatedAt: string;
|
|
119
|
-
}
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
**Pros:**
|
|
123
|
-
- Clean separation
|
|
124
|
-
- No document size concerns
|
|
125
|
-
- Independent updates (no contention)
|
|
126
|
-
|
|
127
|
-
**Cons:**
|
|
128
|
-
- New document type to manage
|
|
129
|
-
- More queries to fetch state
|
|
130
|
-
|
|
131
|
-
---
|
|
132
|
-
|
|
133
|
-
### Option C: Generic Key-Value Store in UserDB
|
|
134
|
-
|
|
135
|
-
Add generic methods for namespaced storage.
|
|
136
|
-
|
|
137
|
-
**Interface:**
|
|
138
|
-
```typescript
|
|
139
|
-
interface UserDBWriter {
|
|
140
|
-
// ... existing methods ...
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Store data in a namespaced location.
|
|
144
|
-
* @param namespace - Unique namespace (e.g., "strategy:interferenceMitigator:course123")
|
|
145
|
-
* @param data - Arbitrary JSON-serializable data
|
|
146
|
-
*/
|
|
147
|
-
putNamespacedData(namespace: string, data: unknown): Promise<void>;
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Retrieve namespaced data.
|
|
151
|
-
* @param namespace - The namespace to retrieve
|
|
152
|
-
* @returns The stored data, or null if not found
|
|
153
|
-
*/
|
|
154
|
-
getNamespacedData<T>(namespace: string): Promise<T | null>;
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* Delete namespaced data.
|
|
158
|
-
* @param namespace - The namespace to delete
|
|
159
|
-
*/
|
|
160
|
-
deleteNamespacedData(namespace: string): Promise<void>;
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
**Pros:**
|
|
165
|
-
- Most flexible
|
|
166
|
-
- Reusable beyond strategies
|
|
167
|
-
- Clean API
|
|
168
|
-
|
|
169
|
-
**Cons:**
|
|
170
|
-
- Very generic (might be too permissive)
|
|
171
|
-
- Namespace collision risk
|
|
172
|
-
|
|
173
|
-
---
|
|
174
|
-
|
|
175
|
-
## Recommended Approach
|
|
176
|
-
|
|
177
|
-
**Option B (Separate Documents)** with a convenience wrapper:
|
|
178
|
-
|
|
179
|
-
```typescript
|
|
180
|
-
// In ContentNavigator base class or a mixin
|
|
181
|
-
abstract class ContentNavigator {
|
|
182
|
-
// ... existing methods ...
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Get this strategy's persisted state for the current course.
|
|
186
|
-
*/
|
|
187
|
-
protected async getStrategyState<T>(): Promise<T | null> {
|
|
188
|
-
const key = `STRATEGY_STATE-${this.course.getCourseID()}-${this.strategyKey}`;
|
|
189
|
-
try {
|
|
190
|
-
return await this.user.get<T>(key);
|
|
191
|
-
} catch (e) {
|
|
192
|
-
return null; // Not found
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Persist this strategy's state for the current course.
|
|
198
|
-
*/
|
|
199
|
-
protected async putStrategyState<T>(data: T): Promise<void> {
|
|
200
|
-
const key = `STRATEGY_STATE-${this.course.getCourseID()}-${this.strategyKey}`;
|
|
201
|
-
const existing = await this.getStrategyState<T>();
|
|
202
|
-
await this.user.put({
|
|
203
|
-
_id: key,
|
|
204
|
-
_rev: existing?._rev,
|
|
205
|
-
docType: DocType.STRATEGY_STATE,
|
|
206
|
-
courseId: this.course.getCourseID(),
|
|
207
|
-
strategyKey: this.strategyKey,
|
|
208
|
-
data,
|
|
209
|
-
updatedAt: new Date().toISOString(),
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Unique key for this strategy instance.
|
|
215
|
-
* Override in subclasses if multiple instances need separate state.
|
|
216
|
-
*/
|
|
217
|
-
protected get strategyKey(): string {
|
|
218
|
-
return this.constructor.name; // e.g., "InterferenceMitigatorNavigator"
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
## Implementation Plan
|
|
224
|
-
|
|
225
|
-
### Phase 1: Add DocType and Interface
|
|
226
|
-
|
|
227
|
-
1. Add `STRATEGY_STATE` to `DocType` enum in `packages/db/src/core/types/types-legacy.ts`
|
|
228
|
-
2. Define `StrategyStateDoc` interface
|
|
229
|
-
3. Add prefix to `DocTypePrefixes`
|
|
230
|
-
|
|
231
|
-
### Phase 2: Add UserDB Methods
|
|
232
|
-
|
|
233
|
-
1. Add `getStrategyState()` and `putStrategyState()` to `UserDBInterface`
|
|
234
|
-
2. Implement in `CouchUserDB`
|
|
235
|
-
|
|
236
|
-
### Phase 3: Add ContentNavigator Helpers
|
|
237
|
-
|
|
238
|
-
1. Add protected helper methods to `ContentNavigator` base class
|
|
239
|
-
2. Document usage pattern
|
|
240
|
-
|
|
241
|
-
### Phase 4: Update Strategies
|
|
242
|
-
|
|
243
|
-
1. Update `InterferenceMitigatorNavigator` to use state storage
|
|
244
|
-
2. Add tests for state persistence
|
|
245
|
-
|
|
246
|
-
## Files to Create/Modify
|
|
247
|
-
|
|
248
|
-
| File | Action | Description |
|
|
249
|
-
|------|--------|-------------|
|
|
250
|
-
| `packages/db/src/core/types/types-legacy.ts` | MODIFY | Add STRATEGY_STATE DocType |
|
|
251
|
-
| `packages/db/src/core/types/strategyState.ts` | CREATE | StrategyStateDoc interface |
|
|
252
|
-
| `packages/db/src/core/interfaces/userDB.ts` | MODIFY | Add state storage methods |
|
|
253
|
-
| `packages/db/src/impl/couch/userDB.ts` | MODIFY | Implement state storage |
|
|
254
|
-
| `packages/db/src/core/navigators/index.ts` | MODIFY | Add helper methods to base class |
|
|
255
|
-
| `packages/db/src/core/navigators/interferenceMitigator.ts` | MODIFY | Use state storage |
|
|
256
|
-
|
|
257
|
-
## Test Cases
|
|
258
|
-
|
|
259
|
-
1. **Store and retrieve**: Write state, read it back, verify equality
|
|
260
|
-
2. **Update existing**: Write state, update it, verify new value
|
|
261
|
-
3. **Separate namespaces**: Two strategies store data, each gets their own
|
|
262
|
-
4. **Cross-session persistence**: Store state, simulate new session, verify data persists
|
|
263
|
-
5. **Missing state**: Read state that doesn't exist, get null (not error)
|
|
264
|
-
|
|
265
|
-
## Open Questions
|
|
266
|
-
|
|
267
|
-
1. **State migration**: How to handle strategy state when strategy config changes?
|
|
268
|
-
2. **State cleanup**: When a strategy is deleted, should its state be cleaned up?
|
|
269
|
-
3. **State size limits**: Should we enforce maximum state size?
|
|
270
|
-
4. **Sync behavior**: How does state sync across devices for multi-device users?
|
|
271
|
-
|
|
272
|
-
## Related Files
|
|
273
|
-
|
|
274
|
-
- `packages/db/src/core/interfaces/userDB.ts` — UserDB interface
|
|
275
|
-
- `packages/db/src/impl/couch/userDB.ts` — UserDB implementation
|
|
276
|
-
- `packages/db/src/core/navigators/index.ts` — ContentNavigator base class
|
|
277
|
-
- `packages/db/src/core/types/types-legacy.ts` — DocType enum
|
|
278
|
-
- `packages/db/docs/navigators-architecture.md` — Architecture overview
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
CourseDBInterface,
|
|
3
|
-
QualifiedCardID,
|
|
4
|
-
StudySessionNewItem,
|
|
5
|
-
StudySessionReviewItem,
|
|
6
|
-
UserDBInterface,
|
|
7
|
-
} from '..';
|
|
8
|
-
import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
|
|
9
|
-
import type { ScheduledCard } from '../types/user';
|
|
10
|
-
import { ContentNavigator } from './index';
|
|
11
|
-
import type { WeightedCard } from './index';
|
|
12
|
-
import type { CardGenerator, GeneratorContext } from './generators/types';
|
|
13
|
-
import { logger } from '../../util/logger';
|
|
14
|
-
|
|
15
|
-
// ============================================================================
|
|
16
|
-
// HARDCODED ORDER NAVIGATOR
|
|
17
|
-
// ============================================================================
|
|
18
|
-
//
|
|
19
|
-
// A generator strategy that presents cards in a fixed, author-defined order.
|
|
20
|
-
//
|
|
21
|
-
// Use case: When course authors want explicit control over content sequencing,
|
|
22
|
-
// e.g., teaching letters in a specific pedagogical order.
|
|
23
|
-
//
|
|
24
|
-
// The order is defined in serializedData as a JSON array of card IDs.
|
|
25
|
-
// Earlier positions in the array get higher scores.
|
|
26
|
-
//
|
|
27
|
-
// ============================================================================
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* A navigation strategy that presents cards in a fixed order.
|
|
31
|
-
*
|
|
32
|
-
* Implements CardGenerator for use in Pipeline architecture.
|
|
33
|
-
* Also extends ContentNavigator for backward compatibility with legacy code.
|
|
34
|
-
*
|
|
35
|
-
* Scoring:
|
|
36
|
-
* - Earlier cards in the sequence get higher scores
|
|
37
|
-
* - Reviews get score 1.0 (highest priority)
|
|
38
|
-
* - New cards scored by position: 1.0 - (position / total) * 0.5
|
|
39
|
-
*/
|
|
40
|
-
export default class HardcodedOrderNavigator extends ContentNavigator implements CardGenerator {
|
|
41
|
-
/** Human-readable name for CardGenerator interface */
|
|
42
|
-
name: string;
|
|
43
|
-
|
|
44
|
-
private orderedCardIds: string[] = [];
|
|
45
|
-
|
|
46
|
-
constructor(
|
|
47
|
-
user: UserDBInterface,
|
|
48
|
-
course: CourseDBInterface,
|
|
49
|
-
strategyData: ContentNavigationStrategyData
|
|
50
|
-
) {
|
|
51
|
-
super(user, course, strategyData);
|
|
52
|
-
this.name = strategyData.name || 'Hardcoded Order';
|
|
53
|
-
|
|
54
|
-
if (strategyData.serializedData) {
|
|
55
|
-
try {
|
|
56
|
-
this.orderedCardIds = JSON.parse(strategyData.serializedData);
|
|
57
|
-
} catch (e) {
|
|
58
|
-
logger.error('Failed to parse serializedData for HardcodedOrderNavigator', e);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
|
|
64
|
-
const reviews = await this.user.getPendingReviews(this.course.getCourseID());
|
|
65
|
-
return reviews.map((r) => {
|
|
66
|
-
return {
|
|
67
|
-
...r,
|
|
68
|
-
contentSourceType: 'course',
|
|
69
|
-
contentSourceID: this.course.getCourseID(),
|
|
70
|
-
cardID: r.cardId,
|
|
71
|
-
courseID: r.courseId,
|
|
72
|
-
reviewID: r._id,
|
|
73
|
-
status: 'review',
|
|
74
|
-
};
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async getNewCards(limit: number = 99): Promise<StudySessionNewItem[]> {
|
|
79
|
-
const activeCardIds = (await this.user.getActiveCards()).map((c: QualifiedCardID) => c.cardID);
|
|
80
|
-
|
|
81
|
-
const newCardIds = this.orderedCardIds.filter((cardId) => !activeCardIds.includes(cardId));
|
|
82
|
-
|
|
83
|
-
const cardsToReturn = newCardIds.slice(0, limit);
|
|
84
|
-
|
|
85
|
-
return cardsToReturn.map((cardId) => {
|
|
86
|
-
return {
|
|
87
|
-
cardID: cardId,
|
|
88
|
-
courseID: this.course.getCourseID(),
|
|
89
|
-
contentSourceType: 'course',
|
|
90
|
-
contentSourceID: this.course.getCourseID(),
|
|
91
|
-
status: 'new',
|
|
92
|
-
};
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Get cards in hardcoded order with scores based on position.
|
|
98
|
-
*
|
|
99
|
-
* Earlier cards in the sequence get higher scores.
|
|
100
|
-
* Score formula: 1.0 - (position / totalCards) * 0.5
|
|
101
|
-
* This ensures scores range from 1.0 (first card) to 0.5+ (last card).
|
|
102
|
-
*
|
|
103
|
-
* This method supports both the legacy signature (limit only) and the
|
|
104
|
-
* CardGenerator interface signature (limit, context).
|
|
105
|
-
*
|
|
106
|
-
* @param limit - Maximum number of cards to return
|
|
107
|
-
* @param _context - Optional GeneratorContext (currently unused, but required for interface)
|
|
108
|
-
*/
|
|
109
|
-
async getWeightedCards(limit: number, _context?: GeneratorContext): Promise<WeightedCard[]> {
|
|
110
|
-
const activeCardIds = (await this.user.getActiveCards()).map((c: QualifiedCardID) => c.cardID);
|
|
111
|
-
const reviews = await this.getPendingReviews();
|
|
112
|
-
|
|
113
|
-
// Filter out already-active cards
|
|
114
|
-
const newCardIds = this.orderedCardIds.filter((cardId) => !activeCardIds.includes(cardId));
|
|
115
|
-
|
|
116
|
-
const totalCards = newCardIds.length;
|
|
117
|
-
|
|
118
|
-
// Score new cards by position in sequence
|
|
119
|
-
const scoredNew: WeightedCard[] = newCardIds.slice(0, limit).map((cardId, index) => {
|
|
120
|
-
const position = index + 1;
|
|
121
|
-
const score = Math.max(0.5, 1.0 - (index / totalCards) * 0.5);
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
cardId,
|
|
125
|
-
courseId: this.course.getCourseID(),
|
|
126
|
-
score,
|
|
127
|
-
provenance: [
|
|
128
|
-
{
|
|
129
|
-
strategy: 'hardcodedOrder',
|
|
130
|
-
strategyName: this.strategyName || this.name,
|
|
131
|
-
strategyId: this.strategyId || 'NAVIGATION_STRATEGY-hardcoded',
|
|
132
|
-
action: 'generated',
|
|
133
|
-
score,
|
|
134
|
-
reason: `Position ${position} of ${totalCards} in fixed sequence, new card`,
|
|
135
|
-
},
|
|
136
|
-
],
|
|
137
|
-
};
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
// Score reviews at 1.0 (highest priority)
|
|
141
|
-
const scoredReviews: WeightedCard[] = reviews.map((r) => ({
|
|
142
|
-
cardId: r.cardID,
|
|
143
|
-
courseId: r.courseID,
|
|
144
|
-
score: 1.0,
|
|
145
|
-
provenance: [
|
|
146
|
-
{
|
|
147
|
-
strategy: 'hardcodedOrder',
|
|
148
|
-
strategyName: this.strategyName || this.name,
|
|
149
|
-
strategyId: this.strategyId || 'NAVIGATION_STRATEGY-hardcoded',
|
|
150
|
-
action: 'generated',
|
|
151
|
-
score: 1.0,
|
|
152
|
-
reason: 'Scheduled review, highest priority',
|
|
153
|
-
},
|
|
154
|
-
],
|
|
155
|
-
}));
|
|
156
|
-
|
|
157
|
-
// Combine (reviews already sorted at top due to score=1.0)
|
|
158
|
-
const all = [...scoredReviews, ...scoredNew];
|
|
159
|
-
all.sort((a, b) => b.score - a.score);
|
|
160
|
-
|
|
161
|
-
return all.slice(0, limit);
|
|
162
|
-
}
|
|
163
|
-
}
|
package/src/util/tuiLogger.ts
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
// TUI-aware logging utility that redirects logs to file in Node.js
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
import { getAppDataDirectory } from './dataDirectory';
|
|
5
|
-
|
|
6
|
-
let logFile: string | null = null;
|
|
7
|
-
let isNodeEnvironment = false;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Initialize TUI logging - redirect console logs to file in Node.js
|
|
11
|
-
*/
|
|
12
|
-
export function initializeTuiLogging(): void {
|
|
13
|
-
// Detect Node.js environment
|
|
14
|
-
isNodeEnvironment = typeof window === 'undefined' && typeof process !== 'undefined';
|
|
15
|
-
|
|
16
|
-
if (!isNodeEnvironment) {
|
|
17
|
-
return; // Browser environment - keep normal console logging
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
try {
|
|
21
|
-
// Set up log file path
|
|
22
|
-
logFile = path.join(getAppDataDirectory(), 'lastrun.log');
|
|
23
|
-
|
|
24
|
-
// Clear previous log file
|
|
25
|
-
if (fs.existsSync(logFile)) {
|
|
26
|
-
fs.unlinkSync(logFile);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Create initial log entry
|
|
30
|
-
const startTime = new Date().toISOString();
|
|
31
|
-
fs.writeFileSync(logFile, `=== TUI Session Started: ${startTime} ===\n`);
|
|
32
|
-
|
|
33
|
-
// Redirect console methods to file
|
|
34
|
-
const originalConsole = {
|
|
35
|
-
// eslint-disable-next-line no-console
|
|
36
|
-
log: console.log,
|
|
37
|
-
// eslint-disable-next-line no-console
|
|
38
|
-
error: console.error,
|
|
39
|
-
// eslint-disable-next-line no-console
|
|
40
|
-
warn: console.warn,
|
|
41
|
-
// eslint-disable-next-line no-console
|
|
42
|
-
info: console.info
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const writeToLog = (level: string, args: any[]) => {
|
|
46
|
-
const timestamp = new Date().toISOString();
|
|
47
|
-
const message = args.map(arg =>
|
|
48
|
-
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
|
|
49
|
-
).join(' ');
|
|
50
|
-
|
|
51
|
-
const logEntry = `[${timestamp}] ${level}: ${message}\n`;
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
fs.appendFileSync(logFile!, logEntry);
|
|
55
|
-
} catch (err) {
|
|
56
|
-
// Fallback to original console if file write fails
|
|
57
|
-
originalConsole.error('Failed to write to log file:', err);
|
|
58
|
-
originalConsole[level.toLowerCase() as keyof typeof originalConsole](...args);
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
// Override console methods
|
|
63
|
-
// eslint-disable-next-line no-console
|
|
64
|
-
console.log = (...args) => writeToLog('INFO', args);
|
|
65
|
-
// eslint-disable-next-line no-console
|
|
66
|
-
console.info = (...args) => writeToLog('INFO', args);
|
|
67
|
-
// eslint-disable-next-line no-console
|
|
68
|
-
console.warn = (...args) => writeToLog('WARN', args);
|
|
69
|
-
// eslint-disable-next-line no-console
|
|
70
|
-
console.error = (...args) => writeToLog('ERROR', args);
|
|
71
|
-
|
|
72
|
-
// Store original methods for potential restoration
|
|
73
|
-
(console as any)._originalMethods = originalConsole;
|
|
74
|
-
|
|
75
|
-
// eslint-disable-next-line no-console
|
|
76
|
-
console.log('TUI logging initialized - logs redirected to', logFile);
|
|
77
|
-
|
|
78
|
-
} catch (err) {
|
|
79
|
-
// eslint-disable-next-line no-console
|
|
80
|
-
console.error('Failed to initialize TUI logging:', err);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Get the current log file path (for debugging)
|
|
86
|
-
*/
|
|
87
|
-
export function getLogFilePath(): string | null {
|
|
88
|
-
return logFile;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Show user-facing message (always visible in TUI)
|
|
93
|
-
*/
|
|
94
|
-
export function showUserMessage(message: string): void {
|
|
95
|
-
if (isNodeEnvironment) {
|
|
96
|
-
// In Node.js, write directly to stdout to bypass log redirection
|
|
97
|
-
process.stdout.write(message + '\n');
|
|
98
|
-
} else {
|
|
99
|
-
// In browser, use normal console
|
|
100
|
-
// eslint-disable-next-line no-console
|
|
101
|
-
console.log(message);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Show user-facing error (always visible in TUI)
|
|
107
|
-
*/
|
|
108
|
-
export function showUserError(message: string): void {
|
|
109
|
-
if (isNodeEnvironment) {
|
|
110
|
-
// In Node.js, write directly to stderr to bypass log redirection
|
|
111
|
-
process.stderr.write('Error: ' + message + '\n');
|
|
112
|
-
} else {
|
|
113
|
-
// In browser, use normal console
|
|
114
|
-
// eslint-disable-next-line no-console
|
|
115
|
-
console.error(message);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Logger object with standard log levels
|
|
121
|
-
*/
|
|
122
|
-
export const logger = {
|
|
123
|
-
debug: (message: string, ...args: any[]) => {
|
|
124
|
-
// eslint-disable-next-line no-console
|
|
125
|
-
console.log(`[DEBUG] ${message}`, ...args);
|
|
126
|
-
},
|
|
127
|
-
info: (message: string, ...args: any[]) => {
|
|
128
|
-
// eslint-disable-next-line no-console
|
|
129
|
-
console.info(`[INFO] ${message}`, ...args);
|
|
130
|
-
},
|
|
131
|
-
warn: (message: string, ...args: any[]) => {
|
|
132
|
-
// eslint-disable-next-line no-console
|
|
133
|
-
console.warn(`[WARN] ${message}`, ...args);
|
|
134
|
-
},
|
|
135
|
-
error: (message: string, ...args: any[]) => {
|
|
136
|
-
// eslint-disable-next-line no-console
|
|
137
|
-
console.error(`[ERROR] ${message}`, ...args);
|
|
138
|
-
},
|
|
139
|
-
};
|