@vue-skuilder/db 0.1.32-c → 0.1.32-e
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/{dataLayerProvider-BAn-LRh5.d.ts → contentSource-BMlMwSiG.d.cts} +202 -626
- package/dist/{dataLayerProvider-BJqBlMIl.d.cts → contentSource-Ht3N2f-y.d.ts} +202 -626
- package/dist/core/index.d.cts +23 -84
- package/dist/core/index.d.ts +23 -84
- package/dist/core/index.js +476 -1819
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +456 -1803
- package/dist/core/index.mjs.map +1 -1
- package/dist/dataLayerProvider-BEqB8VBR.d.cts +67 -0
- package/dist/dataLayerProvider-DObSXjnf.d.ts +67 -0
- package/dist/impl/couch/index.d.cts +5 -5
- package/dist/impl/couch/index.d.ts +5 -5
- package/dist/impl/couch/index.js +484 -1827
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +460 -1807
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +5 -4
- package/dist/impl/static/index.d.ts +5 -4
- package/dist/impl/static/index.js +458 -1801
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +437 -1784
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-X6wHrURm.d.ts → index-BWvO-_rJ.d.ts} +1 -1
- package/dist/{index-m8MMGxxR.d.cts → index-Ba7hYbHj.d.cts} +1 -1
- package/dist/index.d.cts +461 -11
- package/dist/index.d.ts +461 -11
- package/dist/index.js +9239 -9159
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9129 -9049
- package/dist/index.mjs.map +1 -1
- package/dist/{types-DZ5dUqbL.d.ts → types-CJrLM1Ew.d.ts} +1 -1
- package/dist/{types-ZL8tOPQZ.d.cts → types-W8n-B6HG.d.cts} +1 -1
- package/dist/{types-legacy-C7r0T4OV.d.cts → types-legacy-JXDxinpU.d.cts} +1 -1
- package/dist/{types-legacy-C7r0T4OV.d.ts → types-legacy-JXDxinpU.d.ts} +1 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/package.json +2 -2
- package/src/core/interfaces/contentSource.ts +2 -3
- package/src/core/navigators/Pipeline.ts +60 -6
- package/src/core/navigators/PipelineDebugger.ts +103 -0
- package/src/core/navigators/filters/hierarchyDefinition.ts +2 -1
- package/src/core/navigators/filters/interferenceMitigator.ts +2 -1
- package/src/core/navigators/filters/relativePriority.ts +2 -1
- package/src/core/navigators/filters/userTagPreference.ts +2 -1
- package/src/core/navigators/generators/CompositeGenerator.ts +58 -5
- package/src/core/navigators/generators/elo.ts +7 -7
- package/src/core/navigators/generators/prescribed.ts +124 -35
- package/src/core/navigators/generators/srs.ts +3 -4
- package/src/core/navigators/generators/types.ts +48 -2
- package/src/core/navigators/index.ts +3 -3
- package/src/impl/couch/classroomDB.ts +4 -3
- package/src/impl/couch/courseDB.ts +3 -3
- package/src/impl/static/courseDB.ts +3 -3
- package/src/study/SessionController.ts +5 -27
- package/src/study/TagFilteredContentSource.ts +4 -3
|
@@ -157,4 +157,4 @@ interface QuestionRecord extends CardRecord, Evaluation {
|
|
|
157
157
|
priorAttemps: number;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
export { type
|
|
160
|
+
export { type CardHistory as C, DocType as D, type Field as F, GuestUsername as G, type QualifiedCardID as Q, type SkuilderCourseData as S, type TagStub as T, type Tag as a, DocTypePrefixes as b, type CardRecord as c, type CardData as d, type CourseListData as e, type DisplayableData as f, type DataShapeData as g, type QuestionData as h, type QuestionRecord as i, log as l };
|
|
@@ -157,4 +157,4 @@ interface QuestionRecord extends CardRecord, Evaluation {
|
|
|
157
157
|
priorAttemps: number;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
export { type
|
|
160
|
+
export { type CardHistory as C, DocType as D, type Field as F, GuestUsername as G, type QualifiedCardID as Q, type SkuilderCourseData as S, type TagStub as T, type Tag as a, DocTypePrefixes as b, type CardRecord as c, type CardData as d, type CourseListData as e, type DisplayableData as f, type DataShapeData as g, type QuestionData as h, type QuestionRecord as i, log as l };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-
|
|
2
|
-
export { C as CouchDBToStaticPacker } from '../../index-
|
|
1
|
+
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-W8n-B6HG.cjs';
|
|
2
|
+
export { C as CouchDBToStaticPacker } from '../../index-Ba7hYbHj.cjs';
|
|
3
3
|
import '@vue-skuilder/common';
|
|
4
|
-
import '../../types-legacy-
|
|
4
|
+
import '../../types-legacy-JXDxinpU.cjs';
|
|
5
5
|
import 'moment';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-
|
|
2
|
-
export { C as CouchDBToStaticPacker } from '../../index-
|
|
1
|
+
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-CJrLM1Ew.js';
|
|
2
|
+
export { C as CouchDBToStaticPacker } from '../../index-BWvO-_rJ.js';
|
|
3
3
|
import '@vue-skuilder/common';
|
|
4
|
-
import '../../types-legacy-
|
|
4
|
+
import '../../types-legacy-JXDxinpU.js';
|
|
5
5
|
import 'moment';
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.1.32-
|
|
7
|
+
"version": "0.1.32-e",
|
|
8
8
|
"description": "Database layer for vue-skuilder",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"module": "dist/index.mjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@nilock2/pouchdb-authentication": "^1.0.2",
|
|
51
|
-
"@vue-skuilder/common": "0.1.32-
|
|
51
|
+
"@vue-skuilder/common": "0.1.32-e",
|
|
52
52
|
"cross-fetch": "^4.1.0",
|
|
53
53
|
"moment": "^2.29.4",
|
|
54
54
|
"pouchdb": "^9.0.0",
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import { getDataLayer } from '@db/factory';
|
|
2
2
|
import { UserDBInterface } from '..';
|
|
3
3
|
import { StudentClassroomDB } from '../../impl/couch/classroomDB';
|
|
4
|
-
import {
|
|
4
|
+
import type { GeneratorResult, ReplanHints } from '../navigators/generators/types';
|
|
5
5
|
import { TagFilter, hasActiveFilter } from '@vue-skuilder/common';
|
|
6
6
|
import { TagFilteredContentSource } from '../../study/TagFilteredContentSource';
|
|
7
7
|
import { OrchestrationContext } from '../orchestration';
|
|
8
|
-
import type { ReplanHints } from '@db/study/SessionController';
|
|
9
8
|
|
|
10
9
|
export type StudySessionFailedItem = StudySessionFailedNewItem | StudySessionFailedReviewItem;
|
|
11
10
|
|
|
@@ -73,7 +72,7 @@ export interface StudyContentSource {
|
|
|
73
72
|
* @param limit - Maximum number of cards to return
|
|
74
73
|
* @returns Cards sorted by score descending
|
|
75
74
|
*/
|
|
76
|
-
getWeightedCards(limit: number): Promise<
|
|
75
|
+
getWeightedCards(limit: number): Promise<GeneratorResult>;
|
|
77
76
|
|
|
78
77
|
/**
|
|
79
78
|
* Get the orchestration context for this source.
|
|
@@ -5,6 +5,7 @@ import { ContentNavigator } from './index';
|
|
|
5
5
|
import type { WeightedCard } from './index';
|
|
6
6
|
import type { CardFilter, FilterContext } from './filters/types';
|
|
7
7
|
import type { CardGenerator, GeneratorContext } from './generators/types';
|
|
8
|
+
import type { GeneratorResult } from './generators/types';
|
|
8
9
|
import { logger } from '../../util/logger';
|
|
9
10
|
import { createOrchestrationContext, OrchestrationContext } from '../orchestration';
|
|
10
11
|
import { captureRun, buildRunReport, registerPipelineForDebug, type GeneratorSummary, type FilterImpact } from './PipelineDebugger';
|
|
@@ -22,9 +23,9 @@ import { captureRun, buildRunReport, registerPipelineForDebug, type GeneratorSum
|
|
|
22
23
|
// 'gpc:exercise:t-*' — all t-variant exercises
|
|
23
24
|
//
|
|
24
25
|
|
|
25
|
-
// ReplanHints is
|
|
26
|
-
import { ReplanHints } from '
|
|
27
|
-
export { ReplanHints };
|
|
26
|
+
// ReplanHints is defined in generators/types — re-export for consumers that import from Pipeline
|
|
27
|
+
import type { ReplanHints } from './generators/types';
|
|
28
|
+
export type { ReplanHints };
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Convert a glob pattern (with `*` wildcards) to a RegExp.
|
|
@@ -47,6 +48,54 @@ function cardMatchesTagPattern(card: WeightedCard, pattern: string): boolean {
|
|
|
47
48
|
return (card.tags ?? []).some((tag) => globMatch(tag, pattern));
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
function mergeHints(allHints: Array<ReplanHints | null | undefined>): ReplanHints | undefined {
|
|
52
|
+
const defined = allHints.filter((h): h is ReplanHints => h !== null && h !== undefined);
|
|
53
|
+
if (defined.length === 0) return undefined;
|
|
54
|
+
|
|
55
|
+
const merged: ReplanHints = {};
|
|
56
|
+
|
|
57
|
+
const boostTags: Record<string, number> = {};
|
|
58
|
+
for (const hints of defined) {
|
|
59
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
|
|
60
|
+
boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (Object.keys(boostTags).length > 0) {
|
|
64
|
+
merged.boostTags = boostTags;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const boostCards: Record<string, number> = {};
|
|
68
|
+
for (const hints of defined) {
|
|
69
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
|
|
70
|
+
boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (Object.keys(boostCards).length > 0) {
|
|
74
|
+
merged.boostCards = boostCards;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const concatUnique = (
|
|
78
|
+
field: 'requireTags' | 'requireCards' | 'excludeTags' | 'excludeCards'
|
|
79
|
+
): void => {
|
|
80
|
+
const values = defined.flatMap((h) => h[field] ?? []);
|
|
81
|
+
if (values.length > 0) {
|
|
82
|
+
merged[field] = [...new Set(values)];
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
concatUnique('requireTags');
|
|
87
|
+
concatUnique('requireCards');
|
|
88
|
+
concatUnique('excludeTags');
|
|
89
|
+
concatUnique('excludeCards');
|
|
90
|
+
|
|
91
|
+
const labels = defined.map((h) => h._label).filter(Boolean);
|
|
92
|
+
if (labels.length > 0) {
|
|
93
|
+
merged._label = labels.join('; ');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
50
99
|
// ============================================================================
|
|
51
100
|
// PIPELINE LOGGING HELPERS
|
|
52
101
|
// ============================================================================
|
|
@@ -293,7 +342,7 @@ export class Pipeline extends ContentNavigator {
|
|
|
293
342
|
* @param limit - Maximum number of cards to return
|
|
294
343
|
* @returns Cards sorted by score descending
|
|
295
344
|
*/
|
|
296
|
-
async getWeightedCards(limit: number): Promise<
|
|
345
|
+
async getWeightedCards(limit: number): Promise<GeneratorResult> {
|
|
297
346
|
const t0 = performance.now();
|
|
298
347
|
|
|
299
348
|
// Build shared context once
|
|
@@ -311,9 +360,14 @@ export class Pipeline extends ContentNavigator {
|
|
|
311
360
|
);
|
|
312
361
|
|
|
313
362
|
// Get candidates from generator, passing context
|
|
314
|
-
|
|
363
|
+
const generatorResult = await this.generator.getWeightedCards(fetchLimit, context);
|
|
364
|
+
let cards = generatorResult.cards;
|
|
315
365
|
const tGenerate = performance.now();
|
|
316
366
|
const generatedCount = cards.length;
|
|
367
|
+
|
|
368
|
+
// Merge generator-emitted hints with any externally supplied one-shot hints
|
|
369
|
+
const mergedHints = mergeHints([this._ephemeralHints, generatorResult.hints]);
|
|
370
|
+
this._ephemeralHints = mergedHints ?? null;
|
|
317
371
|
|
|
318
372
|
// Capture generator breakdown for debugging (if CompositeGenerator)
|
|
319
373
|
let generatorSummaries: GeneratorSummary[] | undefined;
|
|
@@ -436,7 +490,7 @@ export class Pipeline extends ContentNavigator {
|
|
|
436
490
|
logger.debug(`[Pipeline] Failed to capture debug run: ${e}`);
|
|
437
491
|
}
|
|
438
492
|
|
|
439
|
-
return result;
|
|
493
|
+
return { cards: result };
|
|
440
494
|
}
|
|
441
495
|
|
|
442
496
|
/**
|
|
@@ -36,6 +36,7 @@ export function registerPipelineForDebug(pipeline: Pipeline): void {
|
|
|
36
36
|
// window.skuilder.pipeline.showLastRun()
|
|
37
37
|
// window.skuilder.pipeline.showCard('cardId123')
|
|
38
38
|
// window.skuilder.pipeline.explainReviews()
|
|
39
|
+
// window.skuilder.pipeline.showPrescribed()
|
|
39
40
|
// window.skuilder.pipeline.export()
|
|
40
41
|
//
|
|
41
42
|
// ============================================================================
|
|
@@ -389,6 +390,107 @@ export const pipelineDebugAPI = {
|
|
|
389
390
|
console.groupEnd();
|
|
390
391
|
},
|
|
391
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Show prescribed-related cards from the most recent run.
|
|
395
|
+
*
|
|
396
|
+
* Highlights:
|
|
397
|
+
* - cards directly generated by the prescribed strategy
|
|
398
|
+
* - blocked prescribed targets mentioned in provenance
|
|
399
|
+
* - support tags resolved for blocked targets
|
|
400
|
+
*
|
|
401
|
+
* @param groupId - Optional prescribed group ID filter (e.g. 'intro-core')
|
|
402
|
+
*/
|
|
403
|
+
showPrescribed(groupId?: string): void {
|
|
404
|
+
if (runHistory.length === 0) {
|
|
405
|
+
logger.info('[Pipeline Debug] No runs captured yet.');
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const run = runHistory[0];
|
|
410
|
+
const prescribedCards = run.cards.filter((c) =>
|
|
411
|
+
c.provenance.some((p) => p.strategy === 'prescribed')
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// eslint-disable-next-line no-console
|
|
415
|
+
console.group(`🧭 Prescribed Debug (${run.courseId})`);
|
|
416
|
+
|
|
417
|
+
if (prescribedCards.length === 0) {
|
|
418
|
+
logger.info('No prescribed-generated cards were present in the most recent run.');
|
|
419
|
+
// eslint-disable-next-line no-console
|
|
420
|
+
console.groupEnd();
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const rows = prescribedCards
|
|
425
|
+
.map((card) => {
|
|
426
|
+
const prescribedProv = card.provenance.find((p) => p.strategy === 'prescribed');
|
|
427
|
+
const reason = prescribedProv?.reason ?? '';
|
|
428
|
+
const parsedGroup = reason.match(/group=([^;]+)/)?.[1] ?? 'unknown';
|
|
429
|
+
const mode = reason.match(/mode=([^;]+)/)?.[1] ?? 'unknown';
|
|
430
|
+
const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? 'unknown';
|
|
431
|
+
const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? 'none';
|
|
432
|
+
const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? 'none';
|
|
433
|
+
const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? 'unknown';
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
group: parsedGroup,
|
|
437
|
+
mode,
|
|
438
|
+
cardId: card.cardId,
|
|
439
|
+
selected: card.selected ? 'yes' : 'no',
|
|
440
|
+
finalScore: card.finalScore.toFixed(3),
|
|
441
|
+
blocked,
|
|
442
|
+
blockedTargets,
|
|
443
|
+
supportTags,
|
|
444
|
+
multiplier,
|
|
445
|
+
};
|
|
446
|
+
})
|
|
447
|
+
.filter((row) => !groupId || row.group === groupId)
|
|
448
|
+
.sort((a, b) => Number(b.finalScore) - Number(a.finalScore));
|
|
449
|
+
|
|
450
|
+
if (rows.length === 0) {
|
|
451
|
+
logger.info(
|
|
452
|
+
`[Pipeline Debug] No prescribed cards matched group '${groupId}' in the most recent run.`
|
|
453
|
+
);
|
|
454
|
+
// eslint-disable-next-line no-console
|
|
455
|
+
console.groupEnd();
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// eslint-disable-next-line no-console
|
|
460
|
+
console.table(rows);
|
|
461
|
+
|
|
462
|
+
const selectedRows = rows.filter((r) => r.selected === 'yes');
|
|
463
|
+
const blockedTargetSet = new Set<string>();
|
|
464
|
+
const supportTagSet = new Set<string>();
|
|
465
|
+
|
|
466
|
+
for (const row of rows) {
|
|
467
|
+
if (row.blockedTargets && row.blockedTargets !== 'none') {
|
|
468
|
+
row.blockedTargets
|
|
469
|
+
.split('|')
|
|
470
|
+
.filter(Boolean)
|
|
471
|
+
.forEach((t) => blockedTargetSet.add(t));
|
|
472
|
+
}
|
|
473
|
+
if (row.supportTags && row.supportTags !== 'none') {
|
|
474
|
+
row.supportTags
|
|
475
|
+
.split('|')
|
|
476
|
+
.filter(Boolean)
|
|
477
|
+
.forEach((t) => supportTagSet.add(t));
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
logger.info(`Prescribed cards in run: ${rows.length}`);
|
|
482
|
+
logger.info(`Selected prescribed cards: ${selectedRows.length}`);
|
|
483
|
+
logger.info(
|
|
484
|
+
`Blocked prescribed targets referenced: ${blockedTargetSet.size > 0 ? [...blockedTargetSet].join(', ') : 'none'}`
|
|
485
|
+
);
|
|
486
|
+
logger.info(
|
|
487
|
+
`Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(', ') : 'none'}`
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
// eslint-disable-next-line no-console
|
|
491
|
+
console.groupEnd();
|
|
492
|
+
},
|
|
493
|
+
|
|
392
494
|
/**
|
|
393
495
|
* Show all runs in compact format.
|
|
394
496
|
*/
|
|
@@ -555,6 +657,7 @@ Commands:
|
|
|
555
657
|
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
556
658
|
.showRegistry() Show navigator registry (classes + roles)
|
|
557
659
|
.showStrategies() Show registry + strategy mapping from last run
|
|
660
|
+
.showPrescribed(id?) Show prescribed-generated cards and blocked/support details from last run
|
|
558
661
|
.listRuns() List all captured runs in table format
|
|
559
662
|
.export() Export run history as JSON for bug reports
|
|
560
663
|
.clear() Clear run history
|
|
@@ -4,6 +4,7 @@ import { ContentNavigator } from '../index';
|
|
|
4
4
|
import type { WeightedCard } from '../index';
|
|
5
5
|
import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
|
|
6
6
|
import type { CardFilter, FilterContext } from './types';
|
|
7
|
+
import type { GeneratorResult } from '../generators/types';
|
|
7
8
|
import { toCourseElo } from '@vue-skuilder/common';
|
|
8
9
|
import { logger } from '../../../util/logger';
|
|
9
10
|
|
|
@@ -401,7 +402,7 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
401
402
|
*
|
|
402
403
|
* Use transform() via Pipeline instead.
|
|
403
404
|
*/
|
|
404
|
-
async getWeightedCards(_limit: number): Promise<
|
|
405
|
+
async getWeightedCards(_limit: number): Promise<GeneratorResult> {
|
|
405
406
|
throw new Error(
|
|
406
407
|
'HierarchyDefinitionNavigator is a filter and should not be used as a generator. ' +
|
|
407
408
|
'Use Pipeline with a generator and this filter via transform().'
|
|
@@ -4,6 +4,7 @@ import { ContentNavigator } from '../index';
|
|
|
4
4
|
import type { WeightedCard } from '../index';
|
|
5
5
|
import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
|
|
6
6
|
import type { CardFilter, FilterContext } from './types';
|
|
7
|
+
import type { GeneratorResult } from '../generators/types';
|
|
7
8
|
import { toCourseElo } from '@vue-skuilder/common';
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -332,7 +333,7 @@ export default class InterferenceMitigatorNavigator extends ContentNavigator imp
|
|
|
332
333
|
*
|
|
333
334
|
* Use transform() via Pipeline instead.
|
|
334
335
|
*/
|
|
335
|
-
async getWeightedCards(_limit: number): Promise<
|
|
336
|
+
async getWeightedCards(_limit: number): Promise<GeneratorResult> {
|
|
336
337
|
throw new Error(
|
|
337
338
|
'InterferenceMitigatorNavigator is a filter and should not be used as a generator. ' +
|
|
338
339
|
'Use Pipeline with a generator and this filter via transform().'
|
|
@@ -4,6 +4,7 @@ import { ContentNavigator } from '../index';
|
|
|
4
4
|
import type { WeightedCard } from '../index';
|
|
5
5
|
import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
|
|
6
6
|
import type { CardFilter, FilterContext } from './types';
|
|
7
|
+
import type { GeneratorResult } from '../generators/types';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Configuration for the RelativePriority strategy.
|
|
@@ -238,7 +239,7 @@ export default class RelativePriorityNavigator extends ContentNavigator implemen
|
|
|
238
239
|
*
|
|
239
240
|
* Use transform() via Pipeline instead.
|
|
240
241
|
*/
|
|
241
|
-
async getWeightedCards(_limit: number): Promise<
|
|
242
|
+
async getWeightedCards(_limit: number): Promise<GeneratorResult> {
|
|
242
243
|
throw new Error(
|
|
243
244
|
'RelativePriorityNavigator is a filter and should not be used as a generator. ' +
|
|
244
245
|
'Use Pipeline with a generator and this filter via transform().'
|
|
@@ -4,6 +4,7 @@ import { ContentNavigator } from '../index';
|
|
|
4
4
|
import type { WeightedCard } from '../index';
|
|
5
5
|
import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
|
|
6
6
|
import type { CardFilter, FilterContext } from './types';
|
|
7
|
+
import type { GeneratorResult } from '../generators/types';
|
|
7
8
|
|
|
8
9
|
// ============================================================================
|
|
9
10
|
// USER TAG PREFERENCE FILTER
|
|
@@ -208,7 +209,7 @@ export default class UserTagPreferenceFilter extends ContentNavigator implements
|
|
|
208
209
|
/**
|
|
209
210
|
* Legacy getWeightedCards - throws as filters should not be used as generators.
|
|
210
211
|
*/
|
|
211
|
-
async getWeightedCards(_limit: number): Promise<
|
|
212
|
+
async getWeightedCards(_limit: number): Promise<GeneratorResult> {
|
|
212
213
|
throw new Error(
|
|
213
214
|
'UserTagPreferenceFilter is a filter and should not be used as a generator. ' +
|
|
214
215
|
'Use Pipeline with a generator and this filter via transform().'
|
|
@@ -3,7 +3,7 @@ import type { WeightedCard } from '../index';
|
|
|
3
3
|
import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
|
|
4
4
|
import type { CourseDBInterface } from '../../interfaces/courseDB';
|
|
5
5
|
import type { UserDBInterface } from '../../interfaces/userDB';
|
|
6
|
-
import type { CardGenerator, GeneratorContext } from './types';
|
|
6
|
+
import type { CardGenerator, GeneratorContext, GeneratorResult, ReplanHints } from './types';
|
|
7
7
|
import { logger } from '../../../util/logger';
|
|
8
8
|
|
|
9
9
|
// ============================================================================
|
|
@@ -37,6 +37,54 @@ export enum AggregationMode {
|
|
|
37
37
|
const DEFAULT_AGGREGATION_MODE = AggregationMode.FREQUENCY_BOOST;
|
|
38
38
|
const FREQUENCY_BOOST_FACTOR = 0.1;
|
|
39
39
|
|
|
40
|
+
function mergeHints(allHints: Array<ReplanHints | undefined>): ReplanHints | undefined {
|
|
41
|
+
const defined = allHints.filter((h): h is ReplanHints => h !== undefined);
|
|
42
|
+
if (defined.length === 0) return undefined;
|
|
43
|
+
|
|
44
|
+
const merged: ReplanHints = {};
|
|
45
|
+
|
|
46
|
+
const boostTags: Record<string, number> = {};
|
|
47
|
+
for (const hints of defined) {
|
|
48
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
|
|
49
|
+
boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (Object.keys(boostTags).length > 0) {
|
|
53
|
+
merged.boostTags = boostTags;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const boostCards: Record<string, number> = {};
|
|
57
|
+
for (const hints of defined) {
|
|
58
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
|
|
59
|
+
boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (Object.keys(boostCards).length > 0) {
|
|
63
|
+
merged.boostCards = boostCards;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const concatUnique = (
|
|
67
|
+
field: 'requireTags' | 'requireCards' | 'excludeTags' | 'excludeCards'
|
|
68
|
+
): void => {
|
|
69
|
+
const values = defined.flatMap((h) => h[field] ?? []);
|
|
70
|
+
if (values.length > 0) {
|
|
71
|
+
merged[field] = [...new Set(values)];
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
concatUnique('requireTags');
|
|
76
|
+
concatUnique('requireCards');
|
|
77
|
+
concatUnique('excludeTags');
|
|
78
|
+
concatUnique('excludeCards');
|
|
79
|
+
|
|
80
|
+
const labels = defined.map((h) => h._label).filter(Boolean);
|
|
81
|
+
if (labels.length > 0) {
|
|
82
|
+
merged._label = labels.join('; ');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
40
88
|
/**
|
|
41
89
|
* Composes multiple generators into a single generator.
|
|
42
90
|
*
|
|
@@ -100,7 +148,7 @@ export default class CompositeGenerator extends ContentNavigator implements Card
|
|
|
100
148
|
* @param limit - Maximum number of cards to return
|
|
101
149
|
* @param context - GeneratorContext passed to child generators (required when called via Pipeline)
|
|
102
150
|
*/
|
|
103
|
-
async getWeightedCards(limit: number, context?: GeneratorContext): Promise<
|
|
151
|
+
async getWeightedCards(limit: number, context?: GeneratorContext): Promise<GeneratorResult> {
|
|
104
152
|
if (!context) {
|
|
105
153
|
throw new Error(
|
|
106
154
|
'CompositeGenerator.getWeightedCards requires a GeneratorContext. ' +
|
|
@@ -115,7 +163,8 @@ export default class CompositeGenerator extends ContentNavigator implements Card
|
|
|
115
163
|
|
|
116
164
|
// Log per-generator breakdown for transparency
|
|
117
165
|
const generatorSummaries: string[] = [];
|
|
118
|
-
results.forEach((
|
|
166
|
+
results.forEach((result, index) => {
|
|
167
|
+
const cards = result.cards;
|
|
119
168
|
const gen = this.generators[index];
|
|
120
169
|
const genName = gen.name || `Generator ${index}`;
|
|
121
170
|
const newCards = cards.filter((c) => c.provenance[0]?.reason?.includes('new card'));
|
|
@@ -138,7 +187,8 @@ export default class CompositeGenerator extends ContentNavigator implements Card
|
|
|
138
187
|
type WeightedResult = { card: WeightedCard; weight: number };
|
|
139
188
|
const byCardId = new Map<string, WeightedResult[]>();
|
|
140
189
|
|
|
141
|
-
results.forEach((
|
|
190
|
+
results.forEach((result, index) => {
|
|
191
|
+
const cards = result.cards;
|
|
142
192
|
// Access learnable weight if available
|
|
143
193
|
const gen = this.generators[index] as unknown as ContentNavigator;
|
|
144
194
|
|
|
@@ -205,7 +255,10 @@ export default class CompositeGenerator extends ContentNavigator implements Card
|
|
|
205
255
|
}
|
|
206
256
|
|
|
207
257
|
// Sort by score descending and limit
|
|
208
|
-
|
|
258
|
+
const cards = merged.sort((a, b) => b.score - a.score).slice(0, limit);
|
|
259
|
+
const hints = mergeHints(results.map((result) => result.hints));
|
|
260
|
+
|
|
261
|
+
return { cards, hints };
|
|
209
262
|
}
|
|
210
263
|
|
|
211
264
|
/**
|
|
@@ -4,7 +4,7 @@ import { ContentNavigator } from '../index';
|
|
|
4
4
|
import type { WeightedCard } from '../index';
|
|
5
5
|
import { toCourseElo } from '@vue-skuilder/common';
|
|
6
6
|
import type { QualifiedCardID } from '../..';
|
|
7
|
-
import type { CardGenerator, GeneratorContext } from './types';
|
|
7
|
+
import type { CardGenerator, GeneratorContext, GeneratorResult } from './types';
|
|
8
8
|
import { logger } from '@db/util/logger';
|
|
9
9
|
|
|
10
10
|
// ============================================================================
|
|
@@ -65,7 +65,7 @@ export default class ELONavigator extends ContentNavigator implements CardGenera
|
|
|
65
65
|
* @param limit - Maximum number of cards to return
|
|
66
66
|
* @param context - Optional GeneratorContext (used when called via Pipeline)
|
|
67
67
|
*/
|
|
68
|
-
async getWeightedCards(limit: number, context?: GeneratorContext): Promise<
|
|
68
|
+
async getWeightedCards(limit: number, context?: GeneratorContext): Promise<GeneratorResult> {
|
|
69
69
|
// Determine user ELO - from context if available, otherwise fetch
|
|
70
70
|
let userGlobalElo: number;
|
|
71
71
|
if (context?.userElo !== undefined) {
|
|
@@ -125,18 +125,18 @@ export default class ELONavigator extends ContentNavigator implements CardGenera
|
|
|
125
125
|
// Sort by sampling key descending (weighted sample without replacement)
|
|
126
126
|
scored.sort((a, b) => b.score - a.score);
|
|
127
127
|
|
|
128
|
-
const
|
|
128
|
+
const cards = scored.slice(0, limit);
|
|
129
129
|
|
|
130
130
|
// Log summary for transparency
|
|
131
|
-
if (
|
|
132
|
-
const topScores =
|
|
131
|
+
if (cards.length > 0) {
|
|
132
|
+
const topScores = cards.slice(0, 3).map((c) => c.score.toFixed(2)).join(', ');
|
|
133
133
|
logger.info(
|
|
134
|
-
`[ELO] Course ${this.course.getCourseID()}: ${
|
|
134
|
+
`[ELO] Course ${this.course.getCourseID()}: ${cards.length} new cards (top scores: ${topScores})`
|
|
135
135
|
);
|
|
136
136
|
} else {
|
|
137
137
|
logger.info(`[ELO] Course ${this.course.getCourseID()}: No new cards available`);
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
return
|
|
140
|
+
return { cards };
|
|
141
141
|
}
|
|
142
142
|
}
|