@vue-skuilder/db 0.1.20 → 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-CZdMBiTU.d.ts → contentSource-BP9hznNV.d.ts} +150 -196
- package/dist/{classroomDB-PxDZTky3.d.cts → contentSource-DsJadoBU.d.cts} +150 -196
- package/dist/core/index.d.cts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +615 -1758
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +579 -1727
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-D8o6ZnKW.d.ts → dataLayerProvider-CHYrQ5pB.d.cts} +1 -1
- package/dist/{dataLayerProvider-D0MoZMjH.d.cts → dataLayerProvider-MDTxXq2l.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +6 -22
- package/dist/impl/couch/index.d.ts +6 -22
- package/dist/impl/couch/index.js +598 -1769
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +579 -1755
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +22 -6
- package/dist/impl/static/index.d.ts +22 -6
- package/dist/impl/static/index.js +617 -1629
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +607 -1624
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +64 -56
- package/dist/index.d.ts +64 -56
- package/dist/index.js +1000 -2161
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +970 -2127
- 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/docs/navigators-architecture.md +2 -9
- package/package.json +3 -3
- package/src/core/interfaces/classroomDB.ts +5 -13
- package/src/core/interfaces/contentSource.ts +6 -66
- package/src/core/interfaces/courseDB.ts +2 -7
- package/src/core/navigators/Pipeline.ts +24 -53
- 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} +11 -25
- package/src/core/navigators/{interferenceMitigator.ts → filters/interferenceMitigator.ts} +10 -24
- package/src/core/navigators/{relativePriority.ts → filters/relativePriority.ts} +10 -24
- package/src/core/navigators/filters/userTagPreference.ts +1 -16
- 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 +36 -91
- package/src/impl/couch/classroomDB.ts +100 -103
- package/src/impl/couch/courseDB.ts +5 -81
- package/src/impl/couch/pouchdb-setup.ts +7 -0
- package/src/impl/static/StaticDataUnpacker.ts +50 -1
- package/src/impl/static/courseDB.ts +76 -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 +5 -72
- package/tests/core/navigators/PipelineAssembler.test.ts +8 -58
- package/tests/core/navigators/navigators.test.ts +118 -151
- package/src/core/navigators/hardcodedOrder.ts +0 -163
- package/src/util/tuiLogger.ts +0 -139
- /package/src/core/navigators/{inferredPreference.ts → filters/inferredPreferenceStub.ts} +0 -0
- /package/src/core/navigators/{userGoal.ts → filters/userGoalStub.ts} +0 -0
|
@@ -1,27 +1,23 @@
|
|
|
1
|
-
import { describe, it, expect
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
2
|
import { ContentNavigator, WeightedCard, getCardOrigin } from '../../../src/core/navigators/index';
|
|
3
|
-
import { StudySessionNewItem, StudySessionReviewItem } from '../../../src/core';
|
|
4
|
-
import { ScheduledCard } from '../../../src/core/types/user';
|
|
5
3
|
|
|
6
|
-
// Mock implementation of ContentNavigator for testing
|
|
4
|
+
// Mock implementation of ContentNavigator for testing base class behavior
|
|
7
5
|
class MockNavigator extends ContentNavigator {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
setMockNewCards(cards: StudySessionNewItem[]) {
|
|
12
|
-
this.mockNewCards = cards;
|
|
13
|
-
}
|
|
6
|
+
name: string = 'MockNavigator';
|
|
7
|
+
// Base class no longer has legacy methods - subclasses must implement getWeightedCards
|
|
8
|
+
}
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
10
|
+
// Mock implementation that properly implements getWeightedCards
|
|
11
|
+
class ProperMockNavigator extends ContentNavigator {
|
|
12
|
+
name: string = 'ProperMockNavigator';
|
|
13
|
+
private cards: WeightedCard[] = [];
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
setCards(cards: WeightedCard[]) {
|
|
16
|
+
this.cards = cards;
|
|
21
17
|
}
|
|
22
18
|
|
|
23
|
-
async
|
|
24
|
-
return this.
|
|
19
|
+
async getWeightedCards(limit: number): Promise<WeightedCard[]> {
|
|
20
|
+
return this.cards.slice(0, limit);
|
|
25
21
|
}
|
|
26
22
|
}
|
|
27
23
|
|
|
@@ -77,111 +73,97 @@ describe('WeightedCard', () => {
|
|
|
77
73
|
});
|
|
78
74
|
});
|
|
79
75
|
|
|
80
|
-
describe('ContentNavigator
|
|
81
|
-
|
|
76
|
+
describe('ContentNavigator base class', () => {
|
|
77
|
+
it('should throw error when getWeightedCards is not implemented', async () => {
|
|
78
|
+
const navigator = new MockNavigator();
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
80
|
+
await expect(navigator.getWeightedCards(10)).rejects.toThrow(
|
|
81
|
+
'must implement getWeightedCards()'
|
|
82
|
+
);
|
|
85
83
|
});
|
|
86
84
|
|
|
87
|
-
it('should
|
|
88
|
-
navigator
|
|
89
|
-
navigator.setMockReviews([]);
|
|
90
|
-
|
|
91
|
-
const result = await navigator.getWeightedCards(10);
|
|
85
|
+
it('should throw error mentioning legacy methods have been removed', async () => {
|
|
86
|
+
const navigator = new MockNavigator();
|
|
92
87
|
|
|
93
|
-
expect(
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('should assign score=1.0 to all cards by default', async () => {
|
|
97
|
-
navigator.setMockNewCards([
|
|
98
|
-
{
|
|
99
|
-
cardID: 'card-1',
|
|
100
|
-
courseID: 'course-1',
|
|
101
|
-
contentSourceType: 'course',
|
|
102
|
-
contentSourceID: 'course-1',
|
|
103
|
-
status: 'new',
|
|
104
|
-
},
|
|
105
|
-
{
|
|
106
|
-
cardID: 'card-2',
|
|
107
|
-
courseID: 'course-1',
|
|
108
|
-
contentSourceType: 'course',
|
|
109
|
-
contentSourceID: 'course-1',
|
|
110
|
-
status: 'new',
|
|
111
|
-
},
|
|
112
|
-
]);
|
|
113
|
-
|
|
114
|
-
const result = await navigator.getWeightedCards(10);
|
|
115
|
-
|
|
116
|
-
expect(result).toHaveLength(2);
|
|
117
|
-
expect(result[0].score).toBe(1.0);
|
|
118
|
-
expect(result[1].score).toBe(1.0);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should mark new cards with source="new"', async () => {
|
|
122
|
-
navigator.setMockNewCards([
|
|
123
|
-
{
|
|
124
|
-
cardID: 'card-1',
|
|
125
|
-
courseID: 'course-1',
|
|
126
|
-
contentSourceType: 'course',
|
|
127
|
-
contentSourceID: 'course-1',
|
|
128
|
-
status: 'new',
|
|
129
|
-
},
|
|
130
|
-
]);
|
|
131
|
-
|
|
132
|
-
const result = await navigator.getWeightedCards(10);
|
|
133
|
-
|
|
134
|
-
expect(getCardOrigin(result[0])).toBe('new');
|
|
135
|
-
expect(result[0].provenance[0].reason).toContain('new');
|
|
88
|
+
await expect(navigator.getWeightedCards(10)).rejects.toThrow();
|
|
136
89
|
});
|
|
90
|
+
});
|
|
137
91
|
|
|
138
|
-
|
|
139
|
-
|
|
92
|
+
describe('ContentNavigator.getWeightedCards with proper implementation', () => {
|
|
93
|
+
it('should return cards from implementation', async () => {
|
|
94
|
+
const navigator = new ProperMockNavigator();
|
|
95
|
+
navigator.setCards([
|
|
140
96
|
{
|
|
141
|
-
|
|
142
|
-
courseID: 'course-1',
|
|
143
|
-
contentSourceType: 'course',
|
|
144
|
-
contentSourceID: 'course-1',
|
|
145
|
-
status: 'review',
|
|
146
|
-
reviewID: 'review-id-1',
|
|
147
|
-
_id: 'scheduled-1',
|
|
148
|
-
cardId: 'review-1',
|
|
97
|
+
cardId: 'card-1',
|
|
149
98
|
courseId: 'course-1',
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
99
|
+
score: 0.8,
|
|
100
|
+
provenance: [
|
|
101
|
+
{
|
|
102
|
+
strategy: 'test',
|
|
103
|
+
strategyName: 'Test',
|
|
104
|
+
strategyId: 'TEST',
|
|
105
|
+
action: 'generated',
|
|
106
|
+
score: 0.8,
|
|
107
|
+
reason: 'Test new card',
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
155
111
|
]);
|
|
156
112
|
|
|
157
113
|
const result = await navigator.getWeightedCards(10);
|
|
158
114
|
|
|
159
|
-
expect(
|
|
160
|
-
expect(result[0].
|
|
115
|
+
expect(result).toHaveLength(1);
|
|
116
|
+
expect(result[0].cardId).toBe('card-1');
|
|
117
|
+
expect(result[0].score).toBe(0.8);
|
|
161
118
|
});
|
|
162
119
|
|
|
163
120
|
it('should respect limit parameter', async () => {
|
|
164
|
-
navigator
|
|
121
|
+
const navigator = new ProperMockNavigator();
|
|
122
|
+
navigator.setCards([
|
|
165
123
|
{
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
124
|
+
cardId: 'card-1',
|
|
125
|
+
courseId: 'course-1',
|
|
126
|
+
score: 0.9,
|
|
127
|
+
provenance: [
|
|
128
|
+
{
|
|
129
|
+
strategy: 'test',
|
|
130
|
+
strategyName: 'Test',
|
|
131
|
+
strategyId: 'TEST',
|
|
132
|
+
action: 'generated',
|
|
133
|
+
score: 0.9,
|
|
134
|
+
reason: 'Test new card',
|
|
135
|
+
},
|
|
136
|
+
],
|
|
171
137
|
},
|
|
172
138
|
{
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
139
|
+
cardId: 'card-2',
|
|
140
|
+
courseId: 'course-1',
|
|
141
|
+
score: 0.8,
|
|
142
|
+
provenance: [
|
|
143
|
+
{
|
|
144
|
+
strategy: 'test',
|
|
145
|
+
strategyName: 'Test',
|
|
146
|
+
strategyId: 'TEST',
|
|
147
|
+
action: 'generated',
|
|
148
|
+
score: 0.8,
|
|
149
|
+
reason: 'Test new card',
|
|
150
|
+
},
|
|
151
|
+
],
|
|
178
152
|
},
|
|
179
153
|
{
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
154
|
+
cardId: 'card-3',
|
|
155
|
+
courseId: 'course-1',
|
|
156
|
+
score: 0.7,
|
|
157
|
+
provenance: [
|
|
158
|
+
{
|
|
159
|
+
strategy: 'test',
|
|
160
|
+
strategyName: 'Test',
|
|
161
|
+
strategyId: 'TEST',
|
|
162
|
+
action: 'generated',
|
|
163
|
+
score: 0.7,
|
|
164
|
+
reason: 'Test new card',
|
|
165
|
+
},
|
|
166
|
+
],
|
|
185
167
|
},
|
|
186
168
|
]);
|
|
187
169
|
|
|
@@ -190,57 +172,42 @@ describe('ContentNavigator.getWeightedCards', () => {
|
|
|
190
172
|
expect(result).toHaveLength(2);
|
|
191
173
|
});
|
|
192
174
|
|
|
193
|
-
it('should
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
status: 'review',
|
|
210
|
-
reviewID: 'review-id-1',
|
|
211
|
-
_id: 'scheduled-1',
|
|
212
|
-
cardId: 'review-1',
|
|
213
|
-
courseId: 'course-1',
|
|
214
|
-
scheduledFor: 'course',
|
|
215
|
-
schedulingAgentId: 'agent-1',
|
|
216
|
-
reviewTime: new Date(),
|
|
217
|
-
scheduledAt: new Date(),
|
|
218
|
-
} as unknown as StudySessionReviewItem & ScheduledCard,
|
|
219
|
-
]);
|
|
220
|
-
|
|
221
|
-
const result = await navigator.getWeightedCards(10);
|
|
222
|
-
|
|
223
|
-
expect(result).toHaveLength(2);
|
|
224
|
-
const origins = result.map((c) => getCardOrigin(c));
|
|
225
|
-
expect(origins).toContain('new');
|
|
226
|
-
expect(origins).toContain('review');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('should correctly map cardID to cardId and courseID to courseId', async () => {
|
|
230
|
-
navigator.setMockNewCards([
|
|
231
|
-
{
|
|
232
|
-
cardID: 'CARD-123',
|
|
233
|
-
courseID: 'COURSE-456',
|
|
234
|
-
contentSourceType: 'course',
|
|
235
|
-
contentSourceID: 'COURSE-456',
|
|
236
|
-
status: 'new',
|
|
237
|
-
},
|
|
238
|
-
]);
|
|
175
|
+
it('should correctly identify card origins from provenance', () => {
|
|
176
|
+
const newCard: WeightedCard = {
|
|
177
|
+
cardId: 'new-1',
|
|
178
|
+
courseId: 'course-1',
|
|
179
|
+
score: 1.0,
|
|
180
|
+
provenance: [
|
|
181
|
+
{
|
|
182
|
+
strategy: 'test',
|
|
183
|
+
strategyName: 'Test',
|
|
184
|
+
strategyId: 'TEST',
|
|
185
|
+
action: 'generated',
|
|
186
|
+
score: 1.0,
|
|
187
|
+
reason: 'ELO distance 50, new card',
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
};
|
|
239
191
|
|
|
240
|
-
const
|
|
192
|
+
const reviewCard: WeightedCard = {
|
|
193
|
+
cardId: 'review-1',
|
|
194
|
+
courseId: 'course-1',
|
|
195
|
+
score: 0.8,
|
|
196
|
+
reviewID: 'SCHEDULED_CARD-123',
|
|
197
|
+
provenance: [
|
|
198
|
+
{
|
|
199
|
+
strategy: 'srs',
|
|
200
|
+
strategyName: 'SRS',
|
|
201
|
+
strategyId: 'SRS',
|
|
202
|
+
action: 'generated',
|
|
203
|
+
score: 0.8,
|
|
204
|
+
reason: '48h overdue (interval: 72h), review',
|
|
205
|
+
},
|
|
206
|
+
],
|
|
207
|
+
};
|
|
241
208
|
|
|
242
|
-
expect(
|
|
243
|
-
expect(
|
|
209
|
+
expect(getCardOrigin(newCard)).toBe('new');
|
|
210
|
+
expect(getCardOrigin(reviewCard)).toBe('review');
|
|
244
211
|
});
|
|
245
212
|
});
|
|
246
213
|
|
|
@@ -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
|
-
};
|
|
File without changes
|
|
File without changes
|