@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
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import type { WeightedCard, StrategyContribution } from './index';
|
|
2
|
+
import { logger } from '../../util/logger';
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// PIPELINE DEBUGGER
|
|
6
|
+
// ============================================================================
|
|
7
|
+
//
|
|
8
|
+
// Console-accessible debug API for inspecting pipeline decisions.
|
|
9
|
+
//
|
|
10
|
+
// Exposed as `window.skuilder.pipeline` for interactive exploration.
|
|
11
|
+
//
|
|
12
|
+
// Usage:
|
|
13
|
+
// window.skuilder.pipeline.showLastRun()
|
|
14
|
+
// window.skuilder.pipeline.showCard('cardId123')
|
|
15
|
+
// window.skuilder.pipeline.explainReviews()
|
|
16
|
+
// window.skuilder.pipeline.export()
|
|
17
|
+
//
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Summary of a single generator's contribution.
|
|
22
|
+
*/
|
|
23
|
+
export interface GeneratorSummary {
|
|
24
|
+
name: string;
|
|
25
|
+
cardCount: number;
|
|
26
|
+
newCount: number;
|
|
27
|
+
reviewCount: number;
|
|
28
|
+
topScore: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Summary of a filter's impact on scores.
|
|
33
|
+
*/
|
|
34
|
+
export interface FilterImpact {
|
|
35
|
+
name: string;
|
|
36
|
+
boosted: number;
|
|
37
|
+
penalized: number;
|
|
38
|
+
passed: number;
|
|
39
|
+
removed: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Complete record of a single pipeline execution.
|
|
44
|
+
*/
|
|
45
|
+
export interface PipelineRunReport {
|
|
46
|
+
runId: string;
|
|
47
|
+
timestamp: Date;
|
|
48
|
+
courseId: string;
|
|
49
|
+
courseName?: string;
|
|
50
|
+
|
|
51
|
+
// Generator phase
|
|
52
|
+
generatorName: string;
|
|
53
|
+
generators?: GeneratorSummary[];
|
|
54
|
+
generatedCount: number;
|
|
55
|
+
|
|
56
|
+
// Filter phase
|
|
57
|
+
filters: FilterImpact[];
|
|
58
|
+
|
|
59
|
+
// Results
|
|
60
|
+
finalCount: number;
|
|
61
|
+
reviewsSelected: number;
|
|
62
|
+
newSelected: number;
|
|
63
|
+
|
|
64
|
+
// Full card data for inspection
|
|
65
|
+
cards: Array<{
|
|
66
|
+
cardId: string;
|
|
67
|
+
courseId: string;
|
|
68
|
+
origin: 'new' | 'review' | 'unknown';
|
|
69
|
+
finalScore: number;
|
|
70
|
+
provenance: StrategyContribution[];
|
|
71
|
+
selected: boolean;
|
|
72
|
+
}>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Ring buffer for storing recent pipeline runs.
|
|
77
|
+
*/
|
|
78
|
+
const MAX_RUNS = 10;
|
|
79
|
+
const runHistory: PipelineRunReport[] = [];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Determine card origin from provenance trail.
|
|
83
|
+
*/
|
|
84
|
+
function getOrigin(card: WeightedCard): 'new' | 'review' | 'unknown' {
|
|
85
|
+
const firstEntry = card.provenance[0];
|
|
86
|
+
if (!firstEntry) return 'unknown';
|
|
87
|
+
const reason = firstEntry.reason?.toLowerCase() || '';
|
|
88
|
+
if (reason.includes('new card')) return 'new';
|
|
89
|
+
if (reason.includes('review')) return 'review';
|
|
90
|
+
return 'unknown';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Capture a pipeline run for later inspection.
|
|
95
|
+
*/
|
|
96
|
+
export function captureRun(report: Omit<PipelineRunReport, 'runId' | 'timestamp'>): void {
|
|
97
|
+
const fullReport: PipelineRunReport = {
|
|
98
|
+
...report,
|
|
99
|
+
runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
100
|
+
timestamp: new Date(),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
runHistory.unshift(fullReport);
|
|
104
|
+
if (runHistory.length > MAX_RUNS) {
|
|
105
|
+
runHistory.pop();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build a capture-ready report from pipeline execution data.
|
|
111
|
+
*/
|
|
112
|
+
export function buildRunReport(
|
|
113
|
+
courseId: string,
|
|
114
|
+
courseName: string | undefined,
|
|
115
|
+
generatorName: string,
|
|
116
|
+
generators: GeneratorSummary[] | undefined,
|
|
117
|
+
generatedCount: number,
|
|
118
|
+
filters: FilterImpact[],
|
|
119
|
+
allCards: WeightedCard[],
|
|
120
|
+
selectedCards: WeightedCard[]
|
|
121
|
+
): Omit<PipelineRunReport, 'runId' | 'timestamp'> {
|
|
122
|
+
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
123
|
+
|
|
124
|
+
const cards = allCards.map((card) => ({
|
|
125
|
+
cardId: card.cardId,
|
|
126
|
+
courseId: card.courseId,
|
|
127
|
+
origin: getOrigin(card),
|
|
128
|
+
finalScore: card.score,
|
|
129
|
+
provenance: card.provenance,
|
|
130
|
+
selected: selectedIds.has(card.cardId),
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === 'review').length;
|
|
134
|
+
const newSelected = selectedCards.filter((c) => getOrigin(c) === 'new').length;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
courseId,
|
|
138
|
+
courseName,
|
|
139
|
+
generatorName,
|
|
140
|
+
generators,
|
|
141
|
+
generatedCount,
|
|
142
|
+
filters,
|
|
143
|
+
finalCount: selectedCards.length,
|
|
144
|
+
reviewsSelected,
|
|
145
|
+
newSelected,
|
|
146
|
+
cards,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// CONSOLE API
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Format a provenance trail for console display.
|
|
156
|
+
*/
|
|
157
|
+
function formatProvenance(provenance: StrategyContribution[]): string {
|
|
158
|
+
return provenance
|
|
159
|
+
.map((p) => {
|
|
160
|
+
const actionSymbol =
|
|
161
|
+
p.action === 'generated'
|
|
162
|
+
? '🎲'
|
|
163
|
+
: p.action === 'boosted'
|
|
164
|
+
? '⬆️'
|
|
165
|
+
: p.action === 'penalized'
|
|
166
|
+
? '⬇️'
|
|
167
|
+
: '➡️';
|
|
168
|
+
return ` ${actionSymbol} ${p.strategyName}: ${p.score.toFixed(3)} - ${p.reason}`;
|
|
169
|
+
})
|
|
170
|
+
.join('\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Print summary of a single pipeline run.
|
|
175
|
+
*/
|
|
176
|
+
function printRunSummary(run: PipelineRunReport): void {
|
|
177
|
+
// eslint-disable-next-line no-console
|
|
178
|
+
console.group(`🔍 Pipeline Run: ${run.courseId} (${run.courseName || 'unnamed'})`);
|
|
179
|
+
logger.info(`Run ID: ${run.runId}`);
|
|
180
|
+
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
181
|
+
logger.info(`Generator: ${run.generatorName} → ${run.generatedCount} candidates`);
|
|
182
|
+
|
|
183
|
+
if (run.generators && run.generators.length > 0) {
|
|
184
|
+
// eslint-disable-next-line no-console
|
|
185
|
+
console.group('Generator breakdown:');
|
|
186
|
+
for (const g of run.generators) {
|
|
187
|
+
logger.info(
|
|
188
|
+
` ${g.name}: ${g.cardCount} cards (${g.newCount} new, ${g.reviewCount} reviews, top: ${g.topScore.toFixed(2)})`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
// eslint-disable-next-line no-console
|
|
192
|
+
console.groupEnd();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (run.filters.length > 0) {
|
|
196
|
+
// eslint-disable-next-line no-console
|
|
197
|
+
console.group('Filter impact:');
|
|
198
|
+
for (const f of run.filters) {
|
|
199
|
+
logger.info(` ${f.name}: ↑${f.boosted} ↓${f.penalized} =${f.passed} ✕${f.removed}`);
|
|
200
|
+
}
|
|
201
|
+
// eslint-disable-next-line no-console
|
|
202
|
+
console.groupEnd();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
logger.info(
|
|
206
|
+
`Result: ${run.finalCount} cards selected (${run.newSelected} new, ${run.reviewsSelected} reviews)`
|
|
207
|
+
);
|
|
208
|
+
// eslint-disable-next-line no-console
|
|
209
|
+
console.groupEnd();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Console API object exposed on window.skuilder.pipeline
|
|
214
|
+
*/
|
|
215
|
+
export const pipelineDebugAPI = {
|
|
216
|
+
/**
|
|
217
|
+
* Get raw run history for programmatic access.
|
|
218
|
+
*/
|
|
219
|
+
get runs(): PipelineRunReport[] {
|
|
220
|
+
return [...runHistory];
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Show summary of a specific pipeline run.
|
|
225
|
+
*/
|
|
226
|
+
showRun(idOrIndex: string | number = 0): void {
|
|
227
|
+
if (runHistory.length === 0) {
|
|
228
|
+
logger.info('[Pipeline Debug] No runs captured yet.');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let run: PipelineRunReport | undefined;
|
|
233
|
+
|
|
234
|
+
if (typeof idOrIndex === 'number') {
|
|
235
|
+
run = runHistory[idOrIndex];
|
|
236
|
+
if (!run) {
|
|
237
|
+
logger.info(
|
|
238
|
+
`[Pipeline Debug] No run found at index ${idOrIndex}. History length: ${runHistory.length}`
|
|
239
|
+
);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
run = runHistory.find((r) => r.runId.endsWith(idOrIndex));
|
|
244
|
+
if (!run) {
|
|
245
|
+
logger.info(`[Pipeline Debug] No run found matching ID '${idOrIndex}'.`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
printRunSummary(run);
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Show summary of the last pipeline run.
|
|
255
|
+
*/
|
|
256
|
+
showLastRun(): void {
|
|
257
|
+
this.showRun(0);
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Show detailed provenance for a specific card.
|
|
262
|
+
*/
|
|
263
|
+
showCard(cardId: string): void {
|
|
264
|
+
for (const run of runHistory) {
|
|
265
|
+
const card = run.cards.find((c) => c.cardId === cardId);
|
|
266
|
+
if (card) {
|
|
267
|
+
// eslint-disable-next-line no-console
|
|
268
|
+
console.group(`🎴 Card: ${cardId}`);
|
|
269
|
+
logger.info(`Course: ${card.courseId}`);
|
|
270
|
+
logger.info(`Origin: ${card.origin}`);
|
|
271
|
+
logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
|
|
272
|
+
logger.info(`Selected: ${card.selected ? 'Yes ✅' : 'No ❌'}`);
|
|
273
|
+
logger.info('Provenance:');
|
|
274
|
+
logger.info(formatProvenance(card.provenance));
|
|
275
|
+
// eslint-disable-next-line no-console
|
|
276
|
+
console.groupEnd();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Explain why reviews may or may not have been selected.
|
|
285
|
+
*/
|
|
286
|
+
explainReviews(): void {
|
|
287
|
+
if (runHistory.length === 0) {
|
|
288
|
+
logger.info('[Pipeline Debug] No runs captured yet.');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// eslint-disable-next-line no-console
|
|
293
|
+
console.group('📋 Review Selection Analysis');
|
|
294
|
+
|
|
295
|
+
for (const run of runHistory) {
|
|
296
|
+
// eslint-disable-next-line no-console
|
|
297
|
+
console.group(`Run: ${run.courseId} @ ${run.timestamp.toLocaleTimeString()}`);
|
|
298
|
+
|
|
299
|
+
const allReviews = run.cards.filter((c) => c.origin === 'review');
|
|
300
|
+
const selectedReviews = allReviews.filter((c) => c.selected);
|
|
301
|
+
|
|
302
|
+
if (allReviews.length === 0) {
|
|
303
|
+
logger.info('❌ No reviews were generated. Check SRS logs for why.');
|
|
304
|
+
} else if (selectedReviews.length === 0) {
|
|
305
|
+
logger.info(`⚠️ ${allReviews.length} reviews generated but none selected.`);
|
|
306
|
+
logger.info('Possible reasons:');
|
|
307
|
+
|
|
308
|
+
// Check if new cards scored higher
|
|
309
|
+
const topNewScore = Math.max(
|
|
310
|
+
...run.cards.filter((c) => c.origin === 'new' && c.selected).map((c) => c.finalScore),
|
|
311
|
+
0
|
|
312
|
+
);
|
|
313
|
+
const topReviewScore = Math.max(...allReviews.map((c) => c.finalScore), 0);
|
|
314
|
+
|
|
315
|
+
if (topReviewScore < topNewScore) {
|
|
316
|
+
logger.info(
|
|
317
|
+
` - New cards scored higher (top new: ${topNewScore.toFixed(2)}, top review: ${topReviewScore.toFixed(2)})`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Show top review that didn't make it
|
|
322
|
+
const topReview = allReviews.sort((a, b) => b.finalScore - a.finalScore)[0];
|
|
323
|
+
if (topReview) {
|
|
324
|
+
logger.info(` - Top review score: ${topReview.finalScore.toFixed(3)}`);
|
|
325
|
+
logger.info(' - Its provenance:');
|
|
326
|
+
logger.info(formatProvenance(topReview.provenance));
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
logger.info(`✅ ${selectedReviews.length}/${allReviews.length} reviews selected.`);
|
|
330
|
+
logger.info('Top selected review:');
|
|
331
|
+
const topSelected = selectedReviews.sort((a, b) => b.finalScore - a.finalScore)[0];
|
|
332
|
+
logger.info(formatProvenance(topSelected.provenance));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// eslint-disable-next-line no-console
|
|
336
|
+
console.groupEnd();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// eslint-disable-next-line no-console
|
|
340
|
+
console.groupEnd();
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Show all runs in compact format.
|
|
345
|
+
*/
|
|
346
|
+
listRuns(): void {
|
|
347
|
+
if (runHistory.length === 0) {
|
|
348
|
+
logger.info('[Pipeline Debug] No runs captured yet.');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// eslint-disable-next-line no-console
|
|
353
|
+
console.table(
|
|
354
|
+
runHistory.map((r) => ({
|
|
355
|
+
id: r.runId.slice(-8),
|
|
356
|
+
time: r.timestamp.toLocaleTimeString(),
|
|
357
|
+
course: r.courseName || r.courseId.slice(0, 8),
|
|
358
|
+
generated: r.generatedCount,
|
|
359
|
+
selected: r.finalCount,
|
|
360
|
+
new: r.newSelected,
|
|
361
|
+
reviews: r.reviewsSelected,
|
|
362
|
+
}))
|
|
363
|
+
);
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Export run history as JSON for bug reports.
|
|
368
|
+
*/
|
|
369
|
+
export(): string {
|
|
370
|
+
const json = JSON.stringify(runHistory, null, 2);
|
|
371
|
+
logger.info('[Pipeline Debug] Run history exported. Copy the returned string or use:');
|
|
372
|
+
logger.info(' copy(window.skuilder.pipeline.export())');
|
|
373
|
+
return json;
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Clear run history.
|
|
378
|
+
*/
|
|
379
|
+
clear(): void {
|
|
380
|
+
runHistory.length = 0;
|
|
381
|
+
logger.info('[Pipeline Debug] Run history cleared.');
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Show help.
|
|
386
|
+
*/
|
|
387
|
+
help(): void {
|
|
388
|
+
logger.info(`
|
|
389
|
+
🔧 Pipeline Debug API
|
|
390
|
+
|
|
391
|
+
Commands:
|
|
392
|
+
.showLastRun() Show summary of most recent pipeline run
|
|
393
|
+
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
394
|
+
.showCard(cardId) Show provenance trail for a specific card
|
|
395
|
+
.explainReviews() Analyze why reviews were/weren't selected
|
|
396
|
+
.listRuns() List all captured runs in table format
|
|
397
|
+
.export() Export run history as JSON for bug reports
|
|
398
|
+
.clear() Clear run history
|
|
399
|
+
.runs Access raw run history array
|
|
400
|
+
.help() Show this help message
|
|
401
|
+
|
|
402
|
+
Example:
|
|
403
|
+
window.skuilder.pipeline.showLastRun()
|
|
404
|
+
window.skuilder.pipeline.showRun(1)
|
|
405
|
+
window.skuilder.pipeline.showCard('abc123')
|
|
406
|
+
`);
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// ============================================================================
|
|
411
|
+
// WINDOW MOUNT
|
|
412
|
+
// ============================================================================
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Mount the debug API on window.skuilder.pipeline
|
|
416
|
+
*/
|
|
417
|
+
export function mountPipelineDebugger(): void {
|
|
418
|
+
if (typeof window === 'undefined') return;
|
|
419
|
+
|
|
420
|
+
const win = window as any;
|
|
421
|
+
win.skuilder = win.skuilder || {};
|
|
422
|
+
win.skuilder.pipeline = pipelineDebugAPI;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Auto-mount when module is loaded
|
|
426
|
+
mountPipelineDebugger();
|
|
@@ -113,6 +113,27 @@ export default class CompositeGenerator extends ContentNavigator implements Card
|
|
|
113
113
|
this.generators.map((g) => g.getWeightedCards(limit, context))
|
|
114
114
|
);
|
|
115
115
|
|
|
116
|
+
// Log per-generator breakdown for transparency
|
|
117
|
+
const generatorSummaries: string[] = [];
|
|
118
|
+
results.forEach((cards, index) => {
|
|
119
|
+
const gen = this.generators[index];
|
|
120
|
+
const genName = gen.name || `Generator ${index}`;
|
|
121
|
+
const newCards = cards.filter((c) => c.provenance[0]?.reason?.includes('new card'));
|
|
122
|
+
const reviewCards = cards.filter((c) => c.provenance[0]?.reason?.includes('review'));
|
|
123
|
+
|
|
124
|
+
if (cards.length > 0) {
|
|
125
|
+
const topScore = Math.max(...cards.map((c) => c.score)).toFixed(2);
|
|
126
|
+
const parts: string[] = [];
|
|
127
|
+
if (newCards.length > 0) parts.push(`${newCards.length} new`);
|
|
128
|
+
if (reviewCards.length > 0) parts.push(`${reviewCards.length} reviews`);
|
|
129
|
+
const breakdown = parts.length > 0 ? parts.join(', ') : `${cards.length} cards`;
|
|
130
|
+
generatorSummaries.push(`${genName}: ${breakdown} (top: ${topScore})`);
|
|
131
|
+
} else {
|
|
132
|
+
generatorSummaries.push(`${genName}: 0 cards`);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
logger.info(`[Composite] Generator breakdown: ${generatorSummaries.join(' | ')}`);
|
|
136
|
+
|
|
116
137
|
// Group by cardId, tracking the weight of the generator that produced each instance
|
|
117
138
|
type WeightedResult = { card: WeightedCard; weight: number };
|
|
118
139
|
const byCardId = new Map<string, WeightedResult[]>();
|
|
@@ -5,6 +5,7 @@ import type { WeightedCard } from '../index';
|
|
|
5
5
|
import { toCourseElo } from '@vue-skuilder/common';
|
|
6
6
|
import type { QualifiedCardID } from '../..';
|
|
7
7
|
import type { CardGenerator, GeneratorContext } from './types';
|
|
8
|
+
import { logger } from '@db/util/logger';
|
|
8
9
|
|
|
9
10
|
// ============================================================================
|
|
10
11
|
// ELO NAVIGATOR
|
|
@@ -113,6 +114,18 @@ export default class ELONavigator extends ContentNavigator implements CardGenera
|
|
|
113
114
|
// Sort by score descending
|
|
114
115
|
scored.sort((a, b) => b.score - a.score);
|
|
115
116
|
|
|
116
|
-
|
|
117
|
+
const result = scored.slice(0, limit);
|
|
118
|
+
|
|
119
|
+
// Log summary for transparency
|
|
120
|
+
if (result.length > 0) {
|
|
121
|
+
const topScores = result.slice(0, 3).map((c) => c.score.toFixed(2)).join(', ');
|
|
122
|
+
logger.info(
|
|
123
|
+
`[ELO] Course ${this.course.getCourseID()}: ${result.length} new cards (top scores: ${topScores})`
|
|
124
|
+
);
|
|
125
|
+
} else {
|
|
126
|
+
logger.info(`[ELO] Course ${this.course.getCourseID()}: No new cards available`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return result;
|
|
117
130
|
}
|
|
118
131
|
}
|