@vue-skuilder/db 0.1.16 → 0.1.18
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/{userDB-DNa0XPtn.d.ts → classroomDB-BgfrVb8d.d.ts} +357 -103
- package/dist/{userDB-BqwxtJ_7.d.mts → classroomDB-CTOenngH.d.cts} +358 -104
- package/dist/core/index.d.cts +230 -0
- package/dist/core/index.d.ts +161 -23
- package/dist/core/index.js +1964 -154
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +1925 -121
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BV5iZqt_.d.ts → dataLayerProvider-CZxC9GtB.d.ts} +1 -1
- package/dist/{dataLayerProvider-VlngD19_.d.mts → dataLayerProvider-D6PoCwS6.d.cts} +1 -1
- package/dist/impl/couch/{index.d.mts → index.d.cts} +46 -5
- package/dist/impl/couch/index.d.ts +44 -3
- package/dist/impl/couch/index.js +1971 -171
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +1933 -134
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/{index.d.mts → index.d.cts} +5 -6
- package/dist/impl/static/index.d.ts +2 -3
- package/dist/impl/static/index.js +1614 -119
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +1585 -92
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-Bmll7Xse.d.mts → index-D-Fa4Smt.d.cts} +1 -1
- package/dist/{index.d.mts → index.d.cts} +97 -13
- package/dist/index.d.ts +90 -6
- package/dist/index.js +2085 -153
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2031 -106
- package/dist/index.mjs.map +1 -1
- package/dist/pouch/index.js +3 -3
- package/dist/{types-Dbp5DaRR.d.mts → types-CzPDLAK6.d.cts} +1 -1
- package/dist/util/packer/{index.d.mts → index.d.cts} +3 -3
- package/dist/util/packer/index.js.map +1 -1
- package/dist/util/packer/index.mjs.map +1 -1
- package/docs/brainstorm-navigation-paradigm.md +369 -0
- package/docs/navigators-architecture.md +265 -0
- package/docs/todo-evolutionary-orchestration.md +310 -0
- package/docs/todo-nominal-tag-types.md +121 -0
- package/docs/todo-pipeline-optimization.md +117 -0
- package/docs/todo-strategy-authoring.md +401 -0
- package/docs/todo-strategy-state-storage.md +278 -0
- package/eslint.config.mjs +1 -1
- package/package.json +9 -4
- package/src/core/interfaces/contentSource.ts +88 -4
- package/src/core/interfaces/navigationStrategyManager.ts +0 -5
- package/src/core/navigators/CompositeGenerator.ts +268 -0
- package/src/core/navigators/Pipeline.ts +205 -0
- package/src/core/navigators/PipelineAssembler.ts +194 -0
- package/src/core/navigators/elo.ts +104 -15
- package/src/core/navigators/filters/eloDistance.ts +132 -0
- package/src/core/navigators/filters/index.ts +6 -0
- package/src/core/navigators/filters/types.ts +115 -0
- package/src/core/navigators/generators/index.ts +2 -0
- package/src/core/navigators/generators/types.ts +107 -0
- package/src/core/navigators/hardcodedOrder.ts +111 -12
- package/src/core/navigators/hierarchyDefinition.ts +266 -0
- package/src/core/navigators/index.ts +345 -3
- package/src/core/navigators/interferenceMitigator.ts +367 -0
- package/src/core/navigators/relativePriority.ts +267 -0
- package/src/core/navigators/srs.ts +195 -0
- package/src/impl/couch/classroomDB.ts +51 -0
- package/src/impl/couch/courseDB.ts +117 -39
- package/src/impl/static/courseDB.ts +0 -4
- package/src/study/SessionController.ts +149 -1
- package/src/study/TagFilteredContentSource.ts +255 -0
- package/src/study/index.ts +1 -0
- package/src/util/dataDirectory.test.ts +51 -22
- package/src/util/logger.ts +0 -1
- package/tests/core/navigators/CompositeGenerator.test.ts +455 -0
- package/tests/core/navigators/Pipeline.test.ts +405 -0
- package/tests/core/navigators/PipelineAssembler.test.ts +351 -0
- package/tests/core/navigators/SRSNavigator.test.ts +344 -0
- package/tests/core/navigators/eloDistanceFilter.test.ts +192 -0
- package/tests/core/navigators/navigators.test.ts +710 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +29 -0
- package/dist/core/index.d.mts +0 -92
- /package/dist/{SyncStrategy-CyATpyLQ.d.mts → SyncStrategy-CyATpyLQ.d.cts} +0 -0
- /package/dist/pouch/{index.d.mts → index.d.cts} +0 -0
- /package/dist/{types-legacy-6ettoclI.d.mts → types-legacy-6ettoclI.d.cts} +0 -0
|
@@ -5,23 +5,282 @@ import {
|
|
|
5
5
|
StudySessionReviewItem,
|
|
6
6
|
StudySessionNewItem,
|
|
7
7
|
} from '..';
|
|
8
|
+
|
|
9
|
+
// Re-export filter types
|
|
10
|
+
export type { CardFilter, FilterContext, CardFilterFactory } from './filters/types';
|
|
11
|
+
|
|
12
|
+
// Re-export generator types
|
|
13
|
+
export type { CardGenerator, GeneratorContext, CardGeneratorFactory } from './generators/types';
|
|
14
|
+
|
|
8
15
|
import { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
|
|
9
16
|
import { ScheduledCard } from '../types/user';
|
|
10
17
|
import { logger } from '../../util/logger';
|
|
11
18
|
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// NAVIGATION STRATEGY API
|
|
21
|
+
// ============================================================================
|
|
22
|
+
//
|
|
23
|
+
// This module defines the ContentNavigator base class and the WeightedCard type,
|
|
24
|
+
// which form the foundation of the pluggable navigation strategy system.
|
|
25
|
+
//
|
|
26
|
+
// KEY CONCEPTS:
|
|
27
|
+
//
|
|
28
|
+
// 1. WeightedCard - A card with a suitability score (0-1) and provenance trail.
|
|
29
|
+
// The provenance tracks how each strategy in the pipeline contributed to
|
|
30
|
+
// the card's final score, ensuring transparency and debuggability.
|
|
31
|
+
//
|
|
32
|
+
// 2. ContentNavigator - Abstract base class for backward compatibility.
|
|
33
|
+
// New code should use CardGenerator or CardFilter interfaces directly.
|
|
34
|
+
//
|
|
35
|
+
// 3. CardGenerator vs CardFilter:
|
|
36
|
+
// - Generators (ELO, SRS, HardcodedOrder) produce candidate cards with scores
|
|
37
|
+
// - Filters (Hierarchy, Interference, Priority, EloDistance) transform scores
|
|
38
|
+
//
|
|
39
|
+
// 4. Pipeline architecture:
|
|
40
|
+
// Pipeline(generator, [filter1, filter2, ...]) executes:
|
|
41
|
+
// cards = generator.getWeightedCards()
|
|
42
|
+
// cards = filter1.transform(cards, context)
|
|
43
|
+
// cards = filter2.transform(cards, context)
|
|
44
|
+
// return sorted(cards)
|
|
45
|
+
//
|
|
46
|
+
// 5. Provenance tracking - Each strategy adds an entry explaining its contribution.
|
|
47
|
+
// This makes the system transparent and debuggable.
|
|
48
|
+
//
|
|
49
|
+
// ============================================================================
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Tracks a single strategy's contribution to a card's final score.
|
|
53
|
+
*
|
|
54
|
+
* Each strategy in the pipeline adds a StrategyContribution entry to the
|
|
55
|
+
* card's provenance array, creating an audit trail of scoring decisions.
|
|
56
|
+
*/
|
|
57
|
+
export interface StrategyContribution {
|
|
58
|
+
/**
|
|
59
|
+
* Strategy type (implementing class name).
|
|
60
|
+
* Examples: 'elo', 'hierarchyDefinition', 'interferenceMitigator'
|
|
61
|
+
*/
|
|
62
|
+
strategy: string;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Human-readable name identifying this specific strategy instance.
|
|
66
|
+
* Extracted from ContentNavigationStrategyData.name.
|
|
67
|
+
* Courses may have multiple instances of the same strategy type with
|
|
68
|
+
* different configurations.
|
|
69
|
+
*
|
|
70
|
+
* Examples:
|
|
71
|
+
* - "ELO (default)"
|
|
72
|
+
* - "Interference: b/d/p confusion"
|
|
73
|
+
* - "Interference: phonetic confusables"
|
|
74
|
+
* - "Priority: Common letters first"
|
|
75
|
+
*/
|
|
76
|
+
strategyName: string;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Unique database document ID for this strategy instance.
|
|
80
|
+
* Extracted from ContentNavigationStrategyData._id.
|
|
81
|
+
* Use this to fetch the full strategy configuration document.
|
|
82
|
+
*
|
|
83
|
+
* Examples:
|
|
84
|
+
* - "NAVIGATION_STRATEGY-ELO-default"
|
|
85
|
+
* - "NAVIGATION_STRATEGY-interference-bdp"
|
|
86
|
+
* - "NAVIGATION_STRATEGY-priority-common-letters"
|
|
87
|
+
*/
|
|
88
|
+
strategyId: string;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* What the strategy did:
|
|
92
|
+
* - 'generated': Strategy produced this card (generators only)
|
|
93
|
+
* - 'passed': Strategy evaluated but didn't change score (transparent pass-through)
|
|
94
|
+
* - 'boosted': Strategy increased the score
|
|
95
|
+
* - 'penalized': Strategy decreased the score
|
|
96
|
+
*/
|
|
97
|
+
action: 'generated' | 'passed' | 'boosted' | 'penalized';
|
|
98
|
+
|
|
99
|
+
/** Score after this strategy's processing */
|
|
100
|
+
score: number;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Human-readable explanation of the strategy's decision.
|
|
104
|
+
*
|
|
105
|
+
* Examples:
|
|
106
|
+
* - "ELO distance 75, new card"
|
|
107
|
+
* - "Prerequisites met: letter-sounds"
|
|
108
|
+
* - "Interferes with immature tag 'd' (decay 0.8)"
|
|
109
|
+
* - "High-priority tag 's' (0.95) → boost 1.15x"
|
|
110
|
+
*
|
|
111
|
+
* Required for transparency - silent adjusters are anti-patterns.
|
|
112
|
+
*/
|
|
113
|
+
reason: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* A card with a suitability score and provenance trail.
|
|
118
|
+
*
|
|
119
|
+
* Scores range from 0-1:
|
|
120
|
+
* - 1.0 = fully suitable
|
|
121
|
+
* - 0.0 = hard filter (e.g., prerequisite not met)
|
|
122
|
+
* - 0.5 = neutral
|
|
123
|
+
* - Intermediate values = soft preference
|
|
124
|
+
*
|
|
125
|
+
* Provenance tracks the scoring pipeline:
|
|
126
|
+
* - First entry: Generator that produced the card
|
|
127
|
+
* - Subsequent entries: Filters that transformed the score
|
|
128
|
+
* - Each entry includes action and human-readable reason
|
|
129
|
+
*/
|
|
130
|
+
export interface WeightedCard {
|
|
131
|
+
cardId: string;
|
|
132
|
+
courseId: string;
|
|
133
|
+
/** Suitability score from 0-1 */
|
|
134
|
+
score: number;
|
|
135
|
+
/**
|
|
136
|
+
* Audit trail of strategy contributions.
|
|
137
|
+
* First entry is from the generator, subsequent entries from filters.
|
|
138
|
+
*/
|
|
139
|
+
provenance: StrategyContribution[];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Extract card origin from provenance trail.
|
|
144
|
+
*
|
|
145
|
+
* The first provenance entry (from the generator) indicates whether
|
|
146
|
+
* this is a new card, review, or failed card. We parse the reason
|
|
147
|
+
* string to extract this information.
|
|
148
|
+
*
|
|
149
|
+
* @param card - Card with provenance trail
|
|
150
|
+
* @returns Card origin ('new', 'review', or 'failed')
|
|
151
|
+
*/
|
|
152
|
+
export function getCardOrigin(card: WeightedCard): 'new' | 'review' | 'failed' {
|
|
153
|
+
if (card.provenance.length === 0) {
|
|
154
|
+
throw new Error('Card has no provenance - cannot determine origin');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const firstEntry = card.provenance[0];
|
|
158
|
+
const reason = firstEntry.reason.toLowerCase();
|
|
159
|
+
|
|
160
|
+
if (reason.includes('failed')) {
|
|
161
|
+
return 'failed';
|
|
162
|
+
}
|
|
163
|
+
if (reason.includes('review')) {
|
|
164
|
+
return 'review';
|
|
165
|
+
}
|
|
166
|
+
return 'new';
|
|
167
|
+
}
|
|
168
|
+
|
|
12
169
|
export enum Navigators {
|
|
13
170
|
ELO = 'elo',
|
|
171
|
+
SRS = 'srs',
|
|
14
172
|
HARDCODED = 'hardcodedOrder',
|
|
173
|
+
HIERARCHY = 'hierarchyDefinition',
|
|
174
|
+
INTERFERENCE = 'interferenceMitigator',
|
|
175
|
+
RELATIVE_PRIORITY = 'relativePriority',
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// NAVIGATOR ROLE CLASSIFICATION
|
|
180
|
+
// ============================================================================
|
|
181
|
+
//
|
|
182
|
+
// Navigators are classified as either generators or filters:
|
|
183
|
+
// - Generators: Produce candidate cards (ELO, SRS, HardcodedOrder)
|
|
184
|
+
// - Filters: Transform/score candidates (Hierarchy, Interference, RelativePriority)
|
|
185
|
+
//
|
|
186
|
+
// This classification is used by PipelineAssembler to build pipelines:
|
|
187
|
+
// 1. Instantiate generators (possibly into a CompositeGenerator)
|
|
188
|
+
// 2. Instantiate filters
|
|
189
|
+
// 3. Create Pipeline(generator, filters)
|
|
190
|
+
//
|
|
191
|
+
// ============================================================================
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Role classification for navigation strategies.
|
|
195
|
+
*
|
|
196
|
+
* - GENERATOR: Produces candidate cards with initial scores
|
|
197
|
+
* - FILTER: Transforms cards with score multipliers
|
|
198
|
+
*/
|
|
199
|
+
export enum NavigatorRole {
|
|
200
|
+
GENERATOR = 'generator',
|
|
201
|
+
FILTER = 'filter',
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Registry mapping navigator implementations to their roles.
|
|
206
|
+
*/
|
|
207
|
+
export const NavigatorRoles: Record<Navigators, NavigatorRole> = {
|
|
208
|
+
[Navigators.ELO]: NavigatorRole.GENERATOR,
|
|
209
|
+
[Navigators.SRS]: NavigatorRole.GENERATOR,
|
|
210
|
+
[Navigators.HARDCODED]: NavigatorRole.GENERATOR,
|
|
211
|
+
[Navigators.HIERARCHY]: NavigatorRole.FILTER,
|
|
212
|
+
[Navigators.INTERFERENCE]: NavigatorRole.FILTER,
|
|
213
|
+
[Navigators.RELATIVE_PRIORITY]: NavigatorRole.FILTER,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if a navigator implementation is a generator.
|
|
218
|
+
*
|
|
219
|
+
* @param impl - Navigator implementation name (e.g., 'elo', 'hierarchyDefinition')
|
|
220
|
+
* @returns true if the navigator is a generator, false otherwise
|
|
221
|
+
*/
|
|
222
|
+
export function isGenerator(impl: string): boolean {
|
|
223
|
+
return NavigatorRoles[impl as Navigators] === NavigatorRole.GENERATOR;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Check if a navigator implementation is a filter.
|
|
228
|
+
*
|
|
229
|
+
* @param impl - Navigator implementation name (e.g., 'elo', 'hierarchyDefinition')
|
|
230
|
+
* @returns true if the navigator is a filter, false otherwise
|
|
231
|
+
*/
|
|
232
|
+
export function isFilter(impl: string): boolean {
|
|
233
|
+
return NavigatorRoles[impl as Navigators] === NavigatorRole.FILTER;
|
|
15
234
|
}
|
|
16
235
|
|
|
17
236
|
/**
|
|
18
|
-
*
|
|
237
|
+
* Abstract base class for navigation strategies.
|
|
238
|
+
*
|
|
239
|
+
* This class exists primarily for backward compatibility with legacy code.
|
|
240
|
+
* New code should use CardGenerator or CardFilter interfaces directly.
|
|
241
|
+
*
|
|
242
|
+
* The class implements StudyContentSource for compatibility with SessionController.
|
|
243
|
+
* Once SessionController migrates to use getWeightedCards() exclusively,
|
|
244
|
+
* the legacy methods can be removed.
|
|
19
245
|
*/
|
|
20
246
|
export abstract class ContentNavigator implements StudyContentSource {
|
|
247
|
+
/** User interface for this navigation session */
|
|
248
|
+
protected user?: UserDBInterface;
|
|
249
|
+
|
|
250
|
+
/** Course interface for this navigation session */
|
|
251
|
+
protected course?: CourseDBInterface;
|
|
252
|
+
|
|
253
|
+
/** Human-readable name for this strategy instance (from ContentNavigationStrategyData.name) */
|
|
254
|
+
protected strategyName?: string;
|
|
255
|
+
|
|
256
|
+
/** Unique document ID for this strategy instance (from ContentNavigationStrategyData._id) */
|
|
257
|
+
protected strategyId?: string;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Constructor for standard navigators.
|
|
261
|
+
* Call this from subclass constructors to initialize common fields.
|
|
262
|
+
*
|
|
263
|
+
* Note: CompositeGenerator doesn't use this pattern and should call super() without args.
|
|
264
|
+
*/
|
|
265
|
+
constructor(
|
|
266
|
+
user?: UserDBInterface,
|
|
267
|
+
course?: CourseDBInterface,
|
|
268
|
+
strategyData?: ContentNavigationStrategyData
|
|
269
|
+
) {
|
|
270
|
+
if (user && course && strategyData) {
|
|
271
|
+
this.user = user;
|
|
272
|
+
this.course = course;
|
|
273
|
+
this.strategyName = strategyData.name;
|
|
274
|
+
this.strategyId = strategyData._id;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
21
278
|
/**
|
|
279
|
+
* Factory method to create navigator instances dynamically.
|
|
22
280
|
*
|
|
23
|
-
* @param user
|
|
24
|
-
* @param
|
|
281
|
+
* @param user - User interface
|
|
282
|
+
* @param course - Course interface
|
|
283
|
+
* @param strategyData - Strategy configuration document
|
|
25
284
|
* @returns the runtime object used to steer a study session.
|
|
26
285
|
*/
|
|
27
286
|
static async create(
|
|
@@ -53,6 +312,89 @@ export abstract class ContentNavigator implements StudyContentSource {
|
|
|
53
312
|
return new NavigatorImpl(user, course, strategyData);
|
|
54
313
|
}
|
|
55
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Get cards scheduled for review.
|
|
317
|
+
*
|
|
318
|
+
* @deprecated This method is part of the legacy StudyContentSource interface.
|
|
319
|
+
* New strategies should focus on implementing CardGenerator.getWeightedCards() instead.
|
|
320
|
+
*/
|
|
56
321
|
abstract getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]>;
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Get new cards for introduction.
|
|
325
|
+
*
|
|
326
|
+
* @deprecated This method is part of the legacy StudyContentSource interface.
|
|
327
|
+
* New strategies should focus on implementing CardGenerator.getWeightedCards() instead.
|
|
328
|
+
*
|
|
329
|
+
* @param n - Maximum number of new cards to return
|
|
330
|
+
*/
|
|
57
331
|
abstract getNewCards(n?: number): Promise<StudySessionNewItem[]>;
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get cards with suitability scores and provenance trails.
|
|
335
|
+
*
|
|
336
|
+
* **This is the PRIMARY API for navigation strategies.**
|
|
337
|
+
*
|
|
338
|
+
* Returns cards ranked by suitability score (0-1). Higher scores indicate
|
|
339
|
+
* better candidates for presentation. Each card includes a provenance trail
|
|
340
|
+
* documenting how strategies contributed to the final score.
|
|
341
|
+
*
|
|
342
|
+
* ## For Generators
|
|
343
|
+
* Override this method to generate candidates and compute scores based on
|
|
344
|
+
* your strategy's logic (e.g., ELO proximity, review urgency). Create the
|
|
345
|
+
* initial provenance entry with action='generated'.
|
|
346
|
+
*
|
|
347
|
+
* ## Default Implementation
|
|
348
|
+
* The base class provides a backward-compatible default that:
|
|
349
|
+
* 1. Calls legacy getNewCards() and getPendingReviews()
|
|
350
|
+
* 2. Assigns score=1.0 to all cards
|
|
351
|
+
* 3. Creates minimal provenance from legacy methods
|
|
352
|
+
* 4. Returns combined results up to limit
|
|
353
|
+
*
|
|
354
|
+
* This allows existing strategies to work without modification while
|
|
355
|
+
* new strategies can override with proper scoring and provenance.
|
|
356
|
+
*
|
|
357
|
+
* @param limit - Maximum cards to return
|
|
358
|
+
* @returns Cards sorted by score descending, with provenance trails
|
|
359
|
+
*/
|
|
360
|
+
async getWeightedCards(limit: number): Promise<WeightedCard[]> {
|
|
361
|
+
// Default implementation: delegate to legacy methods, assign score=1.0
|
|
362
|
+
const newCards = await this.getNewCards(limit);
|
|
363
|
+
const reviews = await this.getPendingReviews();
|
|
364
|
+
|
|
365
|
+
const weighted: WeightedCard[] = [
|
|
366
|
+
...newCards.map((c) => ({
|
|
367
|
+
cardId: c.cardID,
|
|
368
|
+
courseId: c.courseID,
|
|
369
|
+
score: 1.0,
|
|
370
|
+
provenance: [
|
|
371
|
+
{
|
|
372
|
+
strategy: 'legacy',
|
|
373
|
+
strategyName: this.strategyName || 'Legacy API',
|
|
374
|
+
strategyId: this.strategyId || 'legacy-fallback',
|
|
375
|
+
action: 'generated' as const,
|
|
376
|
+
score: 1.0,
|
|
377
|
+
reason: 'Generated via legacy getNewCards(), new card',
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
})),
|
|
381
|
+
...reviews.map((r) => ({
|
|
382
|
+
cardId: r.cardID,
|
|
383
|
+
courseId: r.courseID,
|
|
384
|
+
score: 1.0,
|
|
385
|
+
provenance: [
|
|
386
|
+
{
|
|
387
|
+
strategy: 'legacy',
|
|
388
|
+
strategyName: this.strategyName || 'Legacy API',
|
|
389
|
+
strategyId: this.strategyId || 'legacy-fallback',
|
|
390
|
+
action: 'generated' as const,
|
|
391
|
+
score: 1.0,
|
|
392
|
+
reason: 'Generated via legacy getPendingReviews(), review',
|
|
393
|
+
},
|
|
394
|
+
],
|
|
395
|
+
})),
|
|
396
|
+
];
|
|
397
|
+
|
|
398
|
+
return weighted.slice(0, limit);
|
|
399
|
+
}
|
|
58
400
|
}
|