@vue-skuilder/db 0.1.18 → 0.1.20
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/{classroomDB-BgfrVb8d.d.ts → classroomDB-CZdMBiTU.d.ts} +71 -2
- package/dist/{classroomDB-CTOenngH.d.cts → classroomDB-PxDZTky3.d.cts} +71 -2
- package/dist/core/index.d.cts +80 -6
- package/dist/core/index.d.ts +80 -6
- package/dist/core/index.js +370 -52
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +369 -52
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-D6PoCwS6.d.cts → dataLayerProvider-D0MoZMjH.d.cts} +1 -1
- package/dist/{dataLayerProvider-CZxC9GtB.d.ts → dataLayerProvider-D8o6ZnKW.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +4 -3
- package/dist/impl/couch/index.d.ts +4 -3
- package/dist/impl/couch/index.js +371 -55
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +371 -55
- 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 +356 -44
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +356 -44
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-D-Fa4Smt.d.cts → index-B_j6u5E4.d.cts} +1 -1
- package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
- package/dist/index.d.cts +10 -10
- package/dist/index.d.ts +10 -10
- package/dist/index.js +382 -55
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +381 -55
- package/dist/index.mjs.map +1 -1
- package/dist/{types-CzPDLAK6.d.cts → types-Bn0itutr.d.cts} +1 -1
- package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
- package/dist/{types-legacy-6ettoclI.d.cts → types-legacy-DDY4N-Uq.d.cts} +3 -1
- package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.ts} +3 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/docs/navigators-architecture.md +115 -10
- package/package.json +4 -4
- package/src/core/index.ts +1 -0
- package/src/core/interfaces/courseDB.ts +13 -0
- package/src/core/interfaces/userDB.ts +32 -0
- package/src/core/navigators/Pipeline.ts +127 -14
- package/src/core/navigators/filters/index.ts +3 -0
- package/src/core/navigators/filters/userTagPreference.ts +232 -0
- package/src/core/navigators/hierarchyDefinition.ts +4 -4
- package/src/core/navigators/index.ts +59 -0
- package/src/core/navigators/inferredPreference.ts +107 -0
- package/src/core/navigators/interferenceMitigator.ts +1 -13
- package/src/core/navigators/relativePriority.ts +2 -14
- package/src/core/navigators/userGoal.ts +136 -0
- package/src/core/types/strategyState.ts +84 -0
- package/src/core/types/types-legacy.ts +2 -0
- package/src/impl/common/BaseUserDB.ts +74 -7
- package/src/impl/couch/adminDB.ts +1 -2
- package/src/impl/couch/courseDB.ts +30 -10
- package/src/impl/static/courseDB.ts +11 -0
- package/tests/core/navigators/Pipeline.test.ts +1 -0
- package/docs/todo-pipeline-optimization.md +0 -117
- package/docs/todo-strategy-state-storage.md +0 -278
package/dist/core/index.mjs
CHANGED
|
@@ -100,6 +100,7 @@ var init_types_legacy = __esm({
|
|
|
100
100
|
DocType2["SCHEDULED_CARD"] = "SCHEDULED_CARD";
|
|
101
101
|
DocType2["TAG"] = "TAG";
|
|
102
102
|
DocType2["NAVIGATION_STRATEGY"] = "NAVIGATION_STRATEGY";
|
|
103
|
+
DocType2["STRATEGY_STATE"] = "STRATEGY_STATE";
|
|
103
104
|
return DocType2;
|
|
104
105
|
})(DocType || {});
|
|
105
106
|
DocTypePrefixes = {
|
|
@@ -113,7 +114,8 @@ var init_types_legacy = __esm({
|
|
|
113
114
|
["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
|
|
114
115
|
["VIEW" /* VIEW */]: "VIEW",
|
|
115
116
|
["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
|
|
116
|
-
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
|
|
117
|
+
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY",
|
|
118
|
+
["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE"
|
|
117
119
|
};
|
|
118
120
|
}
|
|
119
121
|
});
|
|
@@ -876,6 +878,41 @@ __export(Pipeline_exports, {
|
|
|
876
878
|
Pipeline: () => Pipeline
|
|
877
879
|
});
|
|
878
880
|
import { toCourseElo as toCourseElo2 } from "@vue-skuilder/common";
|
|
881
|
+
function logPipelineConfig(generator, filters) {
|
|
882
|
+
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
883
|
+
logger.info(
|
|
884
|
+
`[Pipeline] Configuration:
|
|
885
|
+
Generator: ${generator.name}
|
|
886
|
+
Filters:${filterList}`
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
function logTagHydration(cards, tagsByCard) {
|
|
890
|
+
const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
|
|
891
|
+
const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
|
|
892
|
+
logger.debug(
|
|
893
|
+
`[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
|
|
897
|
+
const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
|
|
898
|
+
logger.info(
|
|
899
|
+
`[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
function logCardProvenance(cards, maxCards = 3) {
|
|
903
|
+
const cardsToLog = cards.slice(0, maxCards);
|
|
904
|
+
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
905
|
+
for (const card of cardsToLog) {
|
|
906
|
+
logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
|
|
907
|
+
for (const entry of card.provenance) {
|
|
908
|
+
const scoreChange = entry.score.toFixed(3);
|
|
909
|
+
const action = entry.action.padEnd(9);
|
|
910
|
+
logger.debug(
|
|
911
|
+
`[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
|
|
912
|
+
);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
879
916
|
var Pipeline;
|
|
880
917
|
var init_Pipeline = __esm({
|
|
881
918
|
"src/core/navigators/Pipeline.ts"() {
|
|
@@ -899,19 +936,18 @@ var init_Pipeline = __esm({
|
|
|
899
936
|
this.filters = filters;
|
|
900
937
|
this.user = user;
|
|
901
938
|
this.course = course;
|
|
902
|
-
|
|
903
|
-
`[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
|
|
904
|
-
);
|
|
939
|
+
logPipelineConfig(generator, filters);
|
|
905
940
|
}
|
|
906
941
|
/**
|
|
907
942
|
* Get weighted cards by running generator and applying filters.
|
|
908
943
|
*
|
|
909
944
|
* 1. Build shared context (user ELO, etc.)
|
|
910
945
|
* 2. Get candidates from generator (passing context)
|
|
911
|
-
* 3.
|
|
912
|
-
* 4.
|
|
913
|
-
* 5.
|
|
914
|
-
* 6.
|
|
946
|
+
* 3. Batch hydrate tags for all candidates
|
|
947
|
+
* 4. Apply each filter sequentially
|
|
948
|
+
* 5. Remove zero-score cards
|
|
949
|
+
* 6. Sort by score descending
|
|
950
|
+
* 7. Return top N
|
|
915
951
|
*
|
|
916
952
|
* @param limit - Maximum number of cards to return
|
|
917
953
|
* @returns Cards sorted by score descending
|
|
@@ -924,7 +960,9 @@ var init_Pipeline = __esm({
|
|
|
924
960
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
925
961
|
);
|
|
926
962
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
927
|
-
|
|
963
|
+
const generatedCount = cards.length;
|
|
964
|
+
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
965
|
+
cards = await this.hydrateTags(cards);
|
|
928
966
|
for (const filter of this.filters) {
|
|
929
967
|
const beforeCount = cards.length;
|
|
930
968
|
cards = await filter.transform(cards, context);
|
|
@@ -933,11 +971,33 @@ var init_Pipeline = __esm({
|
|
|
933
971
|
cards = cards.filter((c) => c.score > 0);
|
|
934
972
|
cards.sort((a, b) => b.score - a.score);
|
|
935
973
|
const result = cards.slice(0, limit);
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
);
|
|
974
|
+
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
975
|
+
logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
|
|
976
|
+
logCardProvenance(result, 3);
|
|
939
977
|
return result;
|
|
940
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Batch hydrate tags for all cards.
|
|
981
|
+
*
|
|
982
|
+
* Fetches tags for all cards in a single database query and attaches them
|
|
983
|
+
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
984
|
+
* making individual getAppliedTags() calls.
|
|
985
|
+
*
|
|
986
|
+
* @param cards - Cards to hydrate
|
|
987
|
+
* @returns Cards with tags populated
|
|
988
|
+
*/
|
|
989
|
+
async hydrateTags(cards) {
|
|
990
|
+
if (cards.length === 0) {
|
|
991
|
+
return cards;
|
|
992
|
+
}
|
|
993
|
+
const cardIds = cards.map((c) => c.cardId);
|
|
994
|
+
const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
|
|
995
|
+
logTagHydration(cards, tagsByCard);
|
|
996
|
+
return cards.map((card) => ({
|
|
997
|
+
...card,
|
|
998
|
+
tags: tagsByCard.get(card.cardId) ?? []
|
|
999
|
+
}));
|
|
1000
|
+
}
|
|
941
1001
|
/**
|
|
942
1002
|
* Build shared context for generator and filters.
|
|
943
1003
|
*
|
|
@@ -1297,15 +1357,144 @@ var init_eloDistance = __esm({
|
|
|
1297
1357
|
}
|
|
1298
1358
|
});
|
|
1299
1359
|
|
|
1360
|
+
// src/core/navigators/filters/userTagPreference.ts
|
|
1361
|
+
var userTagPreference_exports = {};
|
|
1362
|
+
__export(userTagPreference_exports, {
|
|
1363
|
+
default: () => UserTagPreferenceFilter
|
|
1364
|
+
});
|
|
1365
|
+
var UserTagPreferenceFilter;
|
|
1366
|
+
var init_userTagPreference = __esm({
|
|
1367
|
+
"src/core/navigators/filters/userTagPreference.ts"() {
|
|
1368
|
+
"use strict";
|
|
1369
|
+
init_navigators();
|
|
1370
|
+
UserTagPreferenceFilter = class extends ContentNavigator {
|
|
1371
|
+
_strategyData;
|
|
1372
|
+
/** Human-readable name for CardFilter interface */
|
|
1373
|
+
name;
|
|
1374
|
+
constructor(user, course, strategyData) {
|
|
1375
|
+
super(user, course, strategyData);
|
|
1376
|
+
this._strategyData = strategyData;
|
|
1377
|
+
this.name = strategyData.name || "User Tag Preferences";
|
|
1378
|
+
}
|
|
1379
|
+
/**
|
|
1380
|
+
* Compute multiplier for a card based on its tags and user preferences.
|
|
1381
|
+
* Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
|
|
1382
|
+
*/
|
|
1383
|
+
computeMultiplier(cardTags, boostMap) {
|
|
1384
|
+
const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
|
|
1385
|
+
if (multipliers.length === 0) {
|
|
1386
|
+
return 1;
|
|
1387
|
+
}
|
|
1388
|
+
return Math.max(...multipliers);
|
|
1389
|
+
}
|
|
1390
|
+
/**
|
|
1391
|
+
* Build human-readable reason for the filter's decision.
|
|
1392
|
+
*/
|
|
1393
|
+
buildReason(cardTags, boostMap, multiplier) {
|
|
1394
|
+
const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
|
|
1395
|
+
if (multiplier === 0) {
|
|
1396
|
+
return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
|
|
1397
|
+
}
|
|
1398
|
+
if (multiplier < 1) {
|
|
1399
|
+
return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1400
|
+
}
|
|
1401
|
+
if (multiplier > 1) {
|
|
1402
|
+
return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1403
|
+
}
|
|
1404
|
+
return "No matching user preferences";
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* CardFilter.transform implementation.
|
|
1408
|
+
*
|
|
1409
|
+
* Apply user tag preferences:
|
|
1410
|
+
* 1. Read preferences from strategy state
|
|
1411
|
+
* 2. If no preferences, pass through unchanged
|
|
1412
|
+
* 3. For each card:
|
|
1413
|
+
* - Look up tag in boost record
|
|
1414
|
+
* - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
|
|
1415
|
+
* - If multiple tags match: use max multiplier
|
|
1416
|
+
* - Append provenance with clear reason
|
|
1417
|
+
*/
|
|
1418
|
+
async transform(cards, _context) {
|
|
1419
|
+
const prefs = await this.getStrategyState();
|
|
1420
|
+
if (!prefs || Object.keys(prefs.boost).length === 0) {
|
|
1421
|
+
return cards.map((card) => ({
|
|
1422
|
+
...card,
|
|
1423
|
+
provenance: [
|
|
1424
|
+
...card.provenance,
|
|
1425
|
+
{
|
|
1426
|
+
strategy: "userTagPreference",
|
|
1427
|
+
strategyName: this.strategyName || this.name,
|
|
1428
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1429
|
+
action: "passed",
|
|
1430
|
+
score: card.score,
|
|
1431
|
+
reason: "No user tag preferences configured"
|
|
1432
|
+
}
|
|
1433
|
+
]
|
|
1434
|
+
}));
|
|
1435
|
+
}
|
|
1436
|
+
const adjusted = await Promise.all(
|
|
1437
|
+
cards.map(async (card) => {
|
|
1438
|
+
const cardTags = card.tags ?? [];
|
|
1439
|
+
const multiplier = this.computeMultiplier(cardTags, prefs.boost);
|
|
1440
|
+
const finalScore = Math.min(1, card.score * multiplier);
|
|
1441
|
+
let action;
|
|
1442
|
+
if (multiplier === 0 || multiplier < 1) {
|
|
1443
|
+
action = "penalized";
|
|
1444
|
+
} else if (multiplier > 1) {
|
|
1445
|
+
action = "boosted";
|
|
1446
|
+
} else {
|
|
1447
|
+
action = "passed";
|
|
1448
|
+
}
|
|
1449
|
+
return {
|
|
1450
|
+
...card,
|
|
1451
|
+
score: finalScore,
|
|
1452
|
+
provenance: [
|
|
1453
|
+
...card.provenance,
|
|
1454
|
+
{
|
|
1455
|
+
strategy: "userTagPreference",
|
|
1456
|
+
strategyName: this.strategyName || this.name,
|
|
1457
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1458
|
+
action,
|
|
1459
|
+
score: finalScore,
|
|
1460
|
+
reason: this.buildReason(cardTags, prefs.boost, multiplier)
|
|
1461
|
+
}
|
|
1462
|
+
]
|
|
1463
|
+
};
|
|
1464
|
+
})
|
|
1465
|
+
);
|
|
1466
|
+
return adjusted;
|
|
1467
|
+
}
|
|
1468
|
+
/**
|
|
1469
|
+
* Legacy getWeightedCards - throws as filters should not be used as generators.
|
|
1470
|
+
*/
|
|
1471
|
+
async getWeightedCards(_limit) {
|
|
1472
|
+
throw new Error(
|
|
1473
|
+
"UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
|
|
1474
|
+
);
|
|
1475
|
+
}
|
|
1476
|
+
// Legacy methods - stub implementations since filters don't generate cards
|
|
1477
|
+
async getNewCards(_n) {
|
|
1478
|
+
return [];
|
|
1479
|
+
}
|
|
1480
|
+
async getPendingReviews() {
|
|
1481
|
+
return [];
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
});
|
|
1486
|
+
|
|
1300
1487
|
// src/core/navigators/filters/index.ts
|
|
1301
1488
|
var filters_exports = {};
|
|
1302
1489
|
__export(filters_exports, {
|
|
1490
|
+
UserTagPreferenceFilter: () => UserTagPreferenceFilter,
|
|
1303
1491
|
createEloDistanceFilter: () => createEloDistanceFilter
|
|
1304
1492
|
});
|
|
1305
1493
|
var init_filters = __esm({
|
|
1306
1494
|
"src/core/navigators/filters/index.ts"() {
|
|
1307
1495
|
"use strict";
|
|
1308
1496
|
init_eloDistance();
|
|
1497
|
+
init_userTagPreference();
|
|
1309
1498
|
}
|
|
1310
1499
|
});
|
|
1311
1500
|
|
|
@@ -1538,10 +1727,9 @@ var init_hierarchyDefinition = __esm({
|
|
|
1538
1727
|
/**
|
|
1539
1728
|
* Check if a card is unlocked and generate reason.
|
|
1540
1729
|
*/
|
|
1541
|
-
async checkCardUnlock(
|
|
1730
|
+
async checkCardUnlock(card, course, unlockedTags, masteredTags) {
|
|
1542
1731
|
try {
|
|
1543
|
-
const
|
|
1544
|
-
const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
|
|
1732
|
+
const cardTags = card.tags ?? [];
|
|
1545
1733
|
const lockedTags = cardTags.filter(
|
|
1546
1734
|
(tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
|
|
1547
1735
|
);
|
|
@@ -1578,7 +1766,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1578
1766
|
const gated = [];
|
|
1579
1767
|
for (const card of cards) {
|
|
1580
1768
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
1581
|
-
card
|
|
1769
|
+
card,
|
|
1582
1770
|
context.course,
|
|
1583
1771
|
unlockedTags,
|
|
1584
1772
|
masteredTags
|
|
@@ -1624,6 +1812,19 @@ var init_hierarchyDefinition = __esm({
|
|
|
1624
1812
|
}
|
|
1625
1813
|
});
|
|
1626
1814
|
|
|
1815
|
+
// src/core/navigators/inferredPreference.ts
|
|
1816
|
+
var inferredPreference_exports = {};
|
|
1817
|
+
__export(inferredPreference_exports, {
|
|
1818
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
|
|
1819
|
+
});
|
|
1820
|
+
var INFERRED_PREFERENCE_NAVIGATOR_STUB;
|
|
1821
|
+
var init_inferredPreference = __esm({
|
|
1822
|
+
"src/core/navigators/inferredPreference.ts"() {
|
|
1823
|
+
"use strict";
|
|
1824
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
|
|
1825
|
+
}
|
|
1826
|
+
});
|
|
1827
|
+
|
|
1627
1828
|
// src/core/navigators/interferenceMitigator.ts
|
|
1628
1829
|
var interferenceMitigator_exports = {};
|
|
1629
1830
|
__export(interferenceMitigator_exports, {
|
|
@@ -1755,17 +1956,6 @@ var init_interferenceMitigator = __esm({
|
|
|
1755
1956
|
}
|
|
1756
1957
|
return avoid;
|
|
1757
1958
|
}
|
|
1758
|
-
/**
|
|
1759
|
-
* Get tags for a single card
|
|
1760
|
-
*/
|
|
1761
|
-
async getCardTags(cardId, course) {
|
|
1762
|
-
try {
|
|
1763
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
1764
|
-
return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
|
|
1765
|
-
} catch {
|
|
1766
|
-
return [];
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
1959
|
/**
|
|
1770
1960
|
* Compute interference score reduction for a card.
|
|
1771
1961
|
* Returns: { multiplier, interfering tags, reason }
|
|
@@ -1817,7 +2007,7 @@ var init_interferenceMitigator = __esm({
|
|
|
1817
2007
|
const tagsToAvoid = this.getTagsToAvoid(immatureTags);
|
|
1818
2008
|
const adjusted = [];
|
|
1819
2009
|
for (const card of cards) {
|
|
1820
|
-
const cardTags =
|
|
2010
|
+
const cardTags = card.tags ?? [];
|
|
1821
2011
|
const { multiplier, reason } = this.computeInterferenceEffect(
|
|
1822
2012
|
cardTags,
|
|
1823
2013
|
tagsToAvoid,
|
|
@@ -1962,27 +2152,16 @@ var init_relativePriority = __esm({
|
|
|
1962
2152
|
return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
|
|
1963
2153
|
}
|
|
1964
2154
|
}
|
|
1965
|
-
/**
|
|
1966
|
-
* Get tags for a single card.
|
|
1967
|
-
*/
|
|
1968
|
-
async getCardTags(cardId, course) {
|
|
1969
|
-
try {
|
|
1970
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
1971
|
-
return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
|
|
1972
|
-
} catch {
|
|
1973
|
-
return [];
|
|
1974
|
-
}
|
|
1975
|
-
}
|
|
1976
2155
|
/**
|
|
1977
2156
|
* CardFilter.transform implementation.
|
|
1978
2157
|
*
|
|
1979
2158
|
* Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
|
|
1980
2159
|
* cards with low-priority tags get reduced scores.
|
|
1981
2160
|
*/
|
|
1982
|
-
async transform(cards,
|
|
2161
|
+
async transform(cards, _context) {
|
|
1983
2162
|
const adjusted = await Promise.all(
|
|
1984
2163
|
cards.map(async (card) => {
|
|
1985
|
-
const cardTags =
|
|
2164
|
+
const cardTags = card.tags ?? [];
|
|
1986
2165
|
const priority = this.computeCardPriority(cardTags);
|
|
1987
2166
|
const boostFactor = this.computeBoostFactor(priority);
|
|
1988
2167
|
const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
|
|
@@ -2149,6 +2328,19 @@ var init_srs = __esm({
|
|
|
2149
2328
|
}
|
|
2150
2329
|
});
|
|
2151
2330
|
|
|
2331
|
+
// src/core/navigators/userGoal.ts
|
|
2332
|
+
var userGoal_exports = {};
|
|
2333
|
+
__export(userGoal_exports, {
|
|
2334
|
+
USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
|
|
2335
|
+
});
|
|
2336
|
+
var USER_GOAL_NAVIGATOR_STUB;
|
|
2337
|
+
var init_userGoal = __esm({
|
|
2338
|
+
"src/core/navigators/userGoal.ts"() {
|
|
2339
|
+
"use strict";
|
|
2340
|
+
USER_GOAL_NAVIGATOR_STUB = true;
|
|
2341
|
+
}
|
|
2342
|
+
});
|
|
2343
|
+
|
|
2152
2344
|
// import("./**/*") in src/core/navigators/index.ts
|
|
2153
2345
|
var globImport;
|
|
2154
2346
|
var init_ = __esm({
|
|
@@ -2161,14 +2353,17 @@ var init_ = __esm({
|
|
|
2161
2353
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
2162
2354
|
"./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
|
|
2163
2355
|
"./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
2356
|
+
"./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
|
|
2164
2357
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
2165
2358
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
|
|
2166
2359
|
"./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
|
|
2167
2360
|
"./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
2168
2361
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
|
|
2362
|
+
"./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
|
|
2169
2363
|
"./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
|
|
2170
2364
|
"./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
|
|
2171
|
-
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
2365
|
+
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
2366
|
+
"./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
|
|
2172
2367
|
});
|
|
2173
2368
|
}
|
|
2174
2369
|
});
|
|
@@ -2217,6 +2412,7 @@ var init_navigators = __esm({
|
|
|
2217
2412
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
2218
2413
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
2219
2414
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
2415
|
+
Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
|
|
2220
2416
|
return Navigators2;
|
|
2221
2417
|
})(Navigators || {});
|
|
2222
2418
|
NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
|
|
@@ -2230,7 +2426,8 @@ var init_navigators = __esm({
|
|
|
2230
2426
|
["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
|
|
2231
2427
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
2232
2428
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
2233
|
-
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER
|
|
2429
|
+
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
2430
|
+
["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
|
|
2234
2431
|
};
|
|
2235
2432
|
ContentNavigator = class {
|
|
2236
2433
|
/** User interface for this navigation session */
|
|
@@ -2255,6 +2452,52 @@ var init_navigators = __esm({
|
|
|
2255
2452
|
this.strategyId = strategyData._id;
|
|
2256
2453
|
}
|
|
2257
2454
|
}
|
|
2455
|
+
// ============================================================================
|
|
2456
|
+
// STRATEGY STATE HELPERS
|
|
2457
|
+
// ============================================================================
|
|
2458
|
+
//
|
|
2459
|
+
// These methods allow strategies to persist their own state (user preferences,
|
|
2460
|
+
// learned patterns, temporal tracking) in the user database.
|
|
2461
|
+
//
|
|
2462
|
+
// ============================================================================
|
|
2463
|
+
/**
|
|
2464
|
+
* Unique key identifying this strategy for state storage.
|
|
2465
|
+
*
|
|
2466
|
+
* Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
|
|
2467
|
+
* Override in subclasses if multiple instances of the same strategy type
|
|
2468
|
+
* need separate state storage.
|
|
2469
|
+
*/
|
|
2470
|
+
get strategyKey() {
|
|
2471
|
+
return this.constructor.name;
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* Get this strategy's persisted state for the current course.
|
|
2475
|
+
*
|
|
2476
|
+
* @returns The strategy's data payload, or null if no state exists
|
|
2477
|
+
* @throws Error if user or course is not initialized
|
|
2478
|
+
*/
|
|
2479
|
+
async getStrategyState() {
|
|
2480
|
+
if (!this.user || !this.course) {
|
|
2481
|
+
throw new Error(
|
|
2482
|
+
`Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2483
|
+
);
|
|
2484
|
+
}
|
|
2485
|
+
return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
|
|
2486
|
+
}
|
|
2487
|
+
/**
|
|
2488
|
+
* Persist this strategy's state for the current course.
|
|
2489
|
+
*
|
|
2490
|
+
* @param data - The strategy's data payload to store
|
|
2491
|
+
* @throws Error if user or course is not initialized
|
|
2492
|
+
*/
|
|
2493
|
+
async putStrategyState(data) {
|
|
2494
|
+
if (!this.user || !this.course) {
|
|
2495
|
+
throw new Error(
|
|
2496
|
+
`Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2497
|
+
);
|
|
2498
|
+
}
|
|
2499
|
+
return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
|
|
2500
|
+
}
|
|
2258
2501
|
/**
|
|
2259
2502
|
* Factory method to create navigator instances dynamically.
|
|
2260
2503
|
*
|
|
@@ -2584,15 +2827,6 @@ var init_courseDB = __esm({
|
|
|
2584
2827
|
ret[r.id] = r.doc.id_displayable_data;
|
|
2585
2828
|
}
|
|
2586
2829
|
});
|
|
2587
|
-
await Promise.all(
|
|
2588
|
-
cards.rows.map((r) => {
|
|
2589
|
-
return async () => {
|
|
2590
|
-
if (isSuccessRow(r)) {
|
|
2591
|
-
ret[r.id] = r.doc.id_displayable_data;
|
|
2592
|
-
}
|
|
2593
|
-
};
|
|
2594
|
-
})
|
|
2595
|
-
);
|
|
2596
2830
|
return ret;
|
|
2597
2831
|
}
|
|
2598
2832
|
async getCardsByELO(elo, cardLimit) {
|
|
@@ -2677,6 +2911,28 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
2677
2911
|
throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
|
|
2678
2912
|
}
|
|
2679
2913
|
}
|
|
2914
|
+
async getAppliedTagsBatch(cardIds) {
|
|
2915
|
+
if (cardIds.length === 0) {
|
|
2916
|
+
return /* @__PURE__ */ new Map();
|
|
2917
|
+
}
|
|
2918
|
+
const db = getCourseDB2(this.id);
|
|
2919
|
+
const result = await db.query("getTags", {
|
|
2920
|
+
keys: cardIds,
|
|
2921
|
+
include_docs: false
|
|
2922
|
+
});
|
|
2923
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
2924
|
+
for (const cardId of cardIds) {
|
|
2925
|
+
tagsByCard.set(cardId, []);
|
|
2926
|
+
}
|
|
2927
|
+
for (const row of result.rows) {
|
|
2928
|
+
const cardId = row.key;
|
|
2929
|
+
const tagName = row.value?.name;
|
|
2930
|
+
if (tagName && tagsByCard.has(cardId)) {
|
|
2931
|
+
tagsByCard.get(cardId).push(tagName);
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
return tagsByCard;
|
|
2935
|
+
}
|
|
2680
2936
|
async addTagToCard(cardId, tagId, updateELO) {
|
|
2681
2937
|
return await addTagToCard(
|
|
2682
2938
|
this.id,
|
|
@@ -4263,6 +4519,55 @@ Currently logged-in as ${this._username}.`
|
|
|
4263
4519
|
async updateUserElo(courseId, elo) {
|
|
4264
4520
|
return updateUserElo(this._username, courseId, elo);
|
|
4265
4521
|
}
|
|
4522
|
+
async getStrategyState(courseId, strategyKey) {
|
|
4523
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
4524
|
+
try {
|
|
4525
|
+
const doc = await this.localDB.get(docId);
|
|
4526
|
+
return doc.data;
|
|
4527
|
+
} catch (e) {
|
|
4528
|
+
const err = e;
|
|
4529
|
+
if (err.status === 404) {
|
|
4530
|
+
return null;
|
|
4531
|
+
}
|
|
4532
|
+
throw e;
|
|
4533
|
+
}
|
|
4534
|
+
}
|
|
4535
|
+
async putStrategyState(courseId, strategyKey, data) {
|
|
4536
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
4537
|
+
let existingRev;
|
|
4538
|
+
try {
|
|
4539
|
+
const existing = await this.localDB.get(docId);
|
|
4540
|
+
existingRev = existing._rev;
|
|
4541
|
+
} catch (e) {
|
|
4542
|
+
const err = e;
|
|
4543
|
+
if (err.status !== 404) {
|
|
4544
|
+
throw e;
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
const doc = {
|
|
4548
|
+
_id: docId,
|
|
4549
|
+
_rev: existingRev,
|
|
4550
|
+
docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
|
|
4551
|
+
courseId,
|
|
4552
|
+
strategyKey,
|
|
4553
|
+
data,
|
|
4554
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4555
|
+
};
|
|
4556
|
+
await this.localDB.put(doc);
|
|
4557
|
+
}
|
|
4558
|
+
async deleteStrategyState(courseId, strategyKey) {
|
|
4559
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
4560
|
+
try {
|
|
4561
|
+
const doc = await this.localDB.get(docId);
|
|
4562
|
+
await this.localDB.remove(doc);
|
|
4563
|
+
} catch (e) {
|
|
4564
|
+
const err = e;
|
|
4565
|
+
if (err.status === 404) {
|
|
4566
|
+
return;
|
|
4567
|
+
}
|
|
4568
|
+
throw e;
|
|
4569
|
+
}
|
|
4570
|
+
}
|
|
4266
4571
|
};
|
|
4267
4572
|
userCoursesDoc = "CourseRegistrations";
|
|
4268
4573
|
userClassroomsDoc = "ClassroomRegistrations";
|
|
@@ -4575,6 +4880,16 @@ var init_user = __esm({
|
|
|
4575
4880
|
}
|
|
4576
4881
|
});
|
|
4577
4882
|
|
|
4883
|
+
// src/core/types/strategyState.ts
|
|
4884
|
+
function buildStrategyStateId(courseId, strategyKey) {
|
|
4885
|
+
return `STRATEGY_STATE::${courseId}::${strategyKey}`;
|
|
4886
|
+
}
|
|
4887
|
+
var init_strategyState = __esm({
|
|
4888
|
+
"src/core/types/strategyState.ts"() {
|
|
4889
|
+
"use strict";
|
|
4890
|
+
}
|
|
4891
|
+
});
|
|
4892
|
+
|
|
4578
4893
|
// src/core/bulkImport/cardProcessor.ts
|
|
4579
4894
|
import { Status as Status4 } from "@vue-skuilder/common";
|
|
4580
4895
|
async function importParsedCards(parsedCards, courseDB, config) {
|
|
@@ -4717,6 +5032,7 @@ var init_core = __esm({
|
|
|
4717
5032
|
init_interfaces();
|
|
4718
5033
|
init_types_legacy();
|
|
4719
5034
|
init_user();
|
|
5035
|
+
init_strategyState();
|
|
4720
5036
|
init_Loggable();
|
|
4721
5037
|
init_util();
|
|
4722
5038
|
init_navigators();
|
|
@@ -4734,6 +5050,7 @@ export {
|
|
|
4734
5050
|
NavigatorRoles,
|
|
4735
5051
|
Navigators,
|
|
4736
5052
|
areQuestionRecords,
|
|
5053
|
+
buildStrategyStateId,
|
|
4737
5054
|
docIsDeleted,
|
|
4738
5055
|
getCardHistoryID,
|
|
4739
5056
|
getCardOrigin,
|