@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/index.mjs
CHANGED
|
@@ -100,6 +100,7 @@ var init_types_legacy = __esm({
|
|
|
100
100
|
DocType3["SCHEDULED_CARD"] = "SCHEDULED_CARD";
|
|
101
101
|
DocType3["TAG"] = "TAG";
|
|
102
102
|
DocType3["NAVIGATION_STRATEGY"] = "NAVIGATION_STRATEGY";
|
|
103
|
+
DocType3["STRATEGY_STATE"] = "STRATEGY_STATE";
|
|
103
104
|
return DocType3;
|
|
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
|
});
|
|
@@ -1112,6 +1114,41 @@ __export(Pipeline_exports, {
|
|
|
1112
1114
|
Pipeline: () => Pipeline
|
|
1113
1115
|
});
|
|
1114
1116
|
import { toCourseElo as toCourseElo2 } from "@vue-skuilder/common";
|
|
1117
|
+
function logPipelineConfig(generator, filters) {
|
|
1118
|
+
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
1119
|
+
logger.info(
|
|
1120
|
+
`[Pipeline] Configuration:
|
|
1121
|
+
Generator: ${generator.name}
|
|
1122
|
+
Filters:${filterList}`
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
function logTagHydration(cards, tagsByCard) {
|
|
1126
|
+
const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
|
|
1127
|
+
const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
|
|
1128
|
+
logger.debug(
|
|
1129
|
+
`[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
|
|
1130
|
+
);
|
|
1131
|
+
}
|
|
1132
|
+
function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
|
|
1133
|
+
const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
|
|
1134
|
+
logger.info(
|
|
1135
|
+
`[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
|
|
1136
|
+
);
|
|
1137
|
+
}
|
|
1138
|
+
function logCardProvenance(cards, maxCards = 3) {
|
|
1139
|
+
const cardsToLog = cards.slice(0, maxCards);
|
|
1140
|
+
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
1141
|
+
for (const card of cardsToLog) {
|
|
1142
|
+
logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
|
|
1143
|
+
for (const entry of card.provenance) {
|
|
1144
|
+
const scoreChange = entry.score.toFixed(3);
|
|
1145
|
+
const action = entry.action.padEnd(9);
|
|
1146
|
+
logger.debug(
|
|
1147
|
+
`[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1115
1152
|
var Pipeline;
|
|
1116
1153
|
var init_Pipeline = __esm({
|
|
1117
1154
|
"src/core/navigators/Pipeline.ts"() {
|
|
@@ -1135,19 +1172,18 @@ var init_Pipeline = __esm({
|
|
|
1135
1172
|
this.filters = filters;
|
|
1136
1173
|
this.user = user;
|
|
1137
1174
|
this.course = course;
|
|
1138
|
-
|
|
1139
|
-
`[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
|
|
1140
|
-
);
|
|
1175
|
+
logPipelineConfig(generator, filters);
|
|
1141
1176
|
}
|
|
1142
1177
|
/**
|
|
1143
1178
|
* Get weighted cards by running generator and applying filters.
|
|
1144
1179
|
*
|
|
1145
1180
|
* 1. Build shared context (user ELO, etc.)
|
|
1146
1181
|
* 2. Get candidates from generator (passing context)
|
|
1147
|
-
* 3.
|
|
1148
|
-
* 4.
|
|
1149
|
-
* 5.
|
|
1150
|
-
* 6.
|
|
1182
|
+
* 3. Batch hydrate tags for all candidates
|
|
1183
|
+
* 4. Apply each filter sequentially
|
|
1184
|
+
* 5. Remove zero-score cards
|
|
1185
|
+
* 6. Sort by score descending
|
|
1186
|
+
* 7. Return top N
|
|
1151
1187
|
*
|
|
1152
1188
|
* @param limit - Maximum number of cards to return
|
|
1153
1189
|
* @returns Cards sorted by score descending
|
|
@@ -1160,7 +1196,9 @@ var init_Pipeline = __esm({
|
|
|
1160
1196
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
1161
1197
|
);
|
|
1162
1198
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
1163
|
-
|
|
1199
|
+
const generatedCount = cards.length;
|
|
1200
|
+
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
1201
|
+
cards = await this.hydrateTags(cards);
|
|
1164
1202
|
for (const filter of this.filters) {
|
|
1165
1203
|
const beforeCount = cards.length;
|
|
1166
1204
|
cards = await filter.transform(cards, context);
|
|
@@ -1169,11 +1207,33 @@ var init_Pipeline = __esm({
|
|
|
1169
1207
|
cards = cards.filter((c) => c.score > 0);
|
|
1170
1208
|
cards.sort((a, b) => b.score - a.score);
|
|
1171
1209
|
const result = cards.slice(0, limit);
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
);
|
|
1210
|
+
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
1211
|
+
logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
|
|
1212
|
+
logCardProvenance(result, 3);
|
|
1175
1213
|
return result;
|
|
1176
1214
|
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Batch hydrate tags for all cards.
|
|
1217
|
+
*
|
|
1218
|
+
* Fetches tags for all cards in a single database query and attaches them
|
|
1219
|
+
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
1220
|
+
* making individual getAppliedTags() calls.
|
|
1221
|
+
*
|
|
1222
|
+
* @param cards - Cards to hydrate
|
|
1223
|
+
* @returns Cards with tags populated
|
|
1224
|
+
*/
|
|
1225
|
+
async hydrateTags(cards) {
|
|
1226
|
+
if (cards.length === 0) {
|
|
1227
|
+
return cards;
|
|
1228
|
+
}
|
|
1229
|
+
const cardIds = cards.map((c) => c.cardId);
|
|
1230
|
+
const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
|
|
1231
|
+
logTagHydration(cards, tagsByCard);
|
|
1232
|
+
return cards.map((card) => ({
|
|
1233
|
+
...card,
|
|
1234
|
+
tags: tagsByCard.get(card.cardId) ?? []
|
|
1235
|
+
}));
|
|
1236
|
+
}
|
|
1177
1237
|
/**
|
|
1178
1238
|
* Build shared context for generator and filters.
|
|
1179
1239
|
*
|
|
@@ -1533,15 +1593,144 @@ var init_eloDistance = __esm({
|
|
|
1533
1593
|
}
|
|
1534
1594
|
});
|
|
1535
1595
|
|
|
1596
|
+
// src/core/navigators/filters/userTagPreference.ts
|
|
1597
|
+
var userTagPreference_exports = {};
|
|
1598
|
+
__export(userTagPreference_exports, {
|
|
1599
|
+
default: () => UserTagPreferenceFilter
|
|
1600
|
+
});
|
|
1601
|
+
var UserTagPreferenceFilter;
|
|
1602
|
+
var init_userTagPreference = __esm({
|
|
1603
|
+
"src/core/navigators/filters/userTagPreference.ts"() {
|
|
1604
|
+
"use strict";
|
|
1605
|
+
init_navigators();
|
|
1606
|
+
UserTagPreferenceFilter = class extends ContentNavigator {
|
|
1607
|
+
_strategyData;
|
|
1608
|
+
/** Human-readable name for CardFilter interface */
|
|
1609
|
+
name;
|
|
1610
|
+
constructor(user, course, strategyData) {
|
|
1611
|
+
super(user, course, strategyData);
|
|
1612
|
+
this._strategyData = strategyData;
|
|
1613
|
+
this.name = strategyData.name || "User Tag Preferences";
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Compute multiplier for a card based on its tags and user preferences.
|
|
1617
|
+
* Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
|
|
1618
|
+
*/
|
|
1619
|
+
computeMultiplier(cardTags, boostMap) {
|
|
1620
|
+
const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
|
|
1621
|
+
if (multipliers.length === 0) {
|
|
1622
|
+
return 1;
|
|
1623
|
+
}
|
|
1624
|
+
return Math.max(...multipliers);
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Build human-readable reason for the filter's decision.
|
|
1628
|
+
*/
|
|
1629
|
+
buildReason(cardTags, boostMap, multiplier) {
|
|
1630
|
+
const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
|
|
1631
|
+
if (multiplier === 0) {
|
|
1632
|
+
return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
|
|
1633
|
+
}
|
|
1634
|
+
if (multiplier < 1) {
|
|
1635
|
+
return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1636
|
+
}
|
|
1637
|
+
if (multiplier > 1) {
|
|
1638
|
+
return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1639
|
+
}
|
|
1640
|
+
return "No matching user preferences";
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* CardFilter.transform implementation.
|
|
1644
|
+
*
|
|
1645
|
+
* Apply user tag preferences:
|
|
1646
|
+
* 1. Read preferences from strategy state
|
|
1647
|
+
* 2. If no preferences, pass through unchanged
|
|
1648
|
+
* 3. For each card:
|
|
1649
|
+
* - Look up tag in boost record
|
|
1650
|
+
* - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
|
|
1651
|
+
* - If multiple tags match: use max multiplier
|
|
1652
|
+
* - Append provenance with clear reason
|
|
1653
|
+
*/
|
|
1654
|
+
async transform(cards, _context) {
|
|
1655
|
+
const prefs = await this.getStrategyState();
|
|
1656
|
+
if (!prefs || Object.keys(prefs.boost).length === 0) {
|
|
1657
|
+
return cards.map((card) => ({
|
|
1658
|
+
...card,
|
|
1659
|
+
provenance: [
|
|
1660
|
+
...card.provenance,
|
|
1661
|
+
{
|
|
1662
|
+
strategy: "userTagPreference",
|
|
1663
|
+
strategyName: this.strategyName || this.name,
|
|
1664
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1665
|
+
action: "passed",
|
|
1666
|
+
score: card.score,
|
|
1667
|
+
reason: "No user tag preferences configured"
|
|
1668
|
+
}
|
|
1669
|
+
]
|
|
1670
|
+
}));
|
|
1671
|
+
}
|
|
1672
|
+
const adjusted = await Promise.all(
|
|
1673
|
+
cards.map(async (card) => {
|
|
1674
|
+
const cardTags = card.tags ?? [];
|
|
1675
|
+
const multiplier = this.computeMultiplier(cardTags, prefs.boost);
|
|
1676
|
+
const finalScore = Math.min(1, card.score * multiplier);
|
|
1677
|
+
let action;
|
|
1678
|
+
if (multiplier === 0 || multiplier < 1) {
|
|
1679
|
+
action = "penalized";
|
|
1680
|
+
} else if (multiplier > 1) {
|
|
1681
|
+
action = "boosted";
|
|
1682
|
+
} else {
|
|
1683
|
+
action = "passed";
|
|
1684
|
+
}
|
|
1685
|
+
return {
|
|
1686
|
+
...card,
|
|
1687
|
+
score: finalScore,
|
|
1688
|
+
provenance: [
|
|
1689
|
+
...card.provenance,
|
|
1690
|
+
{
|
|
1691
|
+
strategy: "userTagPreference",
|
|
1692
|
+
strategyName: this.strategyName || this.name,
|
|
1693
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1694
|
+
action,
|
|
1695
|
+
score: finalScore,
|
|
1696
|
+
reason: this.buildReason(cardTags, prefs.boost, multiplier)
|
|
1697
|
+
}
|
|
1698
|
+
]
|
|
1699
|
+
};
|
|
1700
|
+
})
|
|
1701
|
+
);
|
|
1702
|
+
return adjusted;
|
|
1703
|
+
}
|
|
1704
|
+
/**
|
|
1705
|
+
* Legacy getWeightedCards - throws as filters should not be used as generators.
|
|
1706
|
+
*/
|
|
1707
|
+
async getWeightedCards(_limit) {
|
|
1708
|
+
throw new Error(
|
|
1709
|
+
"UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
|
|
1710
|
+
);
|
|
1711
|
+
}
|
|
1712
|
+
// Legacy methods - stub implementations since filters don't generate cards
|
|
1713
|
+
async getNewCards(_n) {
|
|
1714
|
+
return [];
|
|
1715
|
+
}
|
|
1716
|
+
async getPendingReviews() {
|
|
1717
|
+
return [];
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
});
|
|
1722
|
+
|
|
1536
1723
|
// src/core/navigators/filters/index.ts
|
|
1537
1724
|
var filters_exports = {};
|
|
1538
1725
|
__export(filters_exports, {
|
|
1726
|
+
UserTagPreferenceFilter: () => UserTagPreferenceFilter,
|
|
1539
1727
|
createEloDistanceFilter: () => createEloDistanceFilter
|
|
1540
1728
|
});
|
|
1541
1729
|
var init_filters = __esm({
|
|
1542
1730
|
"src/core/navigators/filters/index.ts"() {
|
|
1543
1731
|
"use strict";
|
|
1544
1732
|
init_eloDistance();
|
|
1733
|
+
init_userTagPreference();
|
|
1545
1734
|
}
|
|
1546
1735
|
});
|
|
1547
1736
|
|
|
@@ -1774,10 +1963,9 @@ var init_hierarchyDefinition = __esm({
|
|
|
1774
1963
|
/**
|
|
1775
1964
|
* Check if a card is unlocked and generate reason.
|
|
1776
1965
|
*/
|
|
1777
|
-
async checkCardUnlock(
|
|
1966
|
+
async checkCardUnlock(card, course, unlockedTags, masteredTags) {
|
|
1778
1967
|
try {
|
|
1779
|
-
const
|
|
1780
|
-
const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
|
|
1968
|
+
const cardTags = card.tags ?? [];
|
|
1781
1969
|
const lockedTags = cardTags.filter(
|
|
1782
1970
|
(tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
|
|
1783
1971
|
);
|
|
@@ -1814,7 +2002,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1814
2002
|
const gated = [];
|
|
1815
2003
|
for (const card of cards) {
|
|
1816
2004
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
1817
|
-
card
|
|
2005
|
+
card,
|
|
1818
2006
|
context.course,
|
|
1819
2007
|
unlockedTags,
|
|
1820
2008
|
masteredTags
|
|
@@ -1860,6 +2048,19 @@ var init_hierarchyDefinition = __esm({
|
|
|
1860
2048
|
}
|
|
1861
2049
|
});
|
|
1862
2050
|
|
|
2051
|
+
// src/core/navigators/inferredPreference.ts
|
|
2052
|
+
var inferredPreference_exports = {};
|
|
2053
|
+
__export(inferredPreference_exports, {
|
|
2054
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
|
|
2055
|
+
});
|
|
2056
|
+
var INFERRED_PREFERENCE_NAVIGATOR_STUB;
|
|
2057
|
+
var init_inferredPreference = __esm({
|
|
2058
|
+
"src/core/navigators/inferredPreference.ts"() {
|
|
2059
|
+
"use strict";
|
|
2060
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
|
|
2061
|
+
}
|
|
2062
|
+
});
|
|
2063
|
+
|
|
1863
2064
|
// src/core/navigators/interferenceMitigator.ts
|
|
1864
2065
|
var interferenceMitigator_exports = {};
|
|
1865
2066
|
__export(interferenceMitigator_exports, {
|
|
@@ -1991,17 +2192,6 @@ var init_interferenceMitigator = __esm({
|
|
|
1991
2192
|
}
|
|
1992
2193
|
return avoid;
|
|
1993
2194
|
}
|
|
1994
|
-
/**
|
|
1995
|
-
* Get tags for a single card
|
|
1996
|
-
*/
|
|
1997
|
-
async getCardTags(cardId, course) {
|
|
1998
|
-
try {
|
|
1999
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
2000
|
-
return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
|
|
2001
|
-
} catch {
|
|
2002
|
-
return [];
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
2195
|
/**
|
|
2006
2196
|
* Compute interference score reduction for a card.
|
|
2007
2197
|
* Returns: { multiplier, interfering tags, reason }
|
|
@@ -2053,7 +2243,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2053
2243
|
const tagsToAvoid = this.getTagsToAvoid(immatureTags);
|
|
2054
2244
|
const adjusted = [];
|
|
2055
2245
|
for (const card of cards) {
|
|
2056
|
-
const cardTags =
|
|
2246
|
+
const cardTags = card.tags ?? [];
|
|
2057
2247
|
const { multiplier, reason } = this.computeInterferenceEffect(
|
|
2058
2248
|
cardTags,
|
|
2059
2249
|
tagsToAvoid,
|
|
@@ -2198,27 +2388,16 @@ var init_relativePriority = __esm({
|
|
|
2198
2388
|
return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
|
|
2199
2389
|
}
|
|
2200
2390
|
}
|
|
2201
|
-
/**
|
|
2202
|
-
* Get tags for a single card.
|
|
2203
|
-
*/
|
|
2204
|
-
async getCardTags(cardId, course) {
|
|
2205
|
-
try {
|
|
2206
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
2207
|
-
return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
|
|
2208
|
-
} catch {
|
|
2209
|
-
return [];
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
2391
|
/**
|
|
2213
2392
|
* CardFilter.transform implementation.
|
|
2214
2393
|
*
|
|
2215
2394
|
* Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
|
|
2216
2395
|
* cards with low-priority tags get reduced scores.
|
|
2217
2396
|
*/
|
|
2218
|
-
async transform(cards,
|
|
2397
|
+
async transform(cards, _context) {
|
|
2219
2398
|
const adjusted = await Promise.all(
|
|
2220
2399
|
cards.map(async (card) => {
|
|
2221
|
-
const cardTags =
|
|
2400
|
+
const cardTags = card.tags ?? [];
|
|
2222
2401
|
const priority = this.computeCardPriority(cardTags);
|
|
2223
2402
|
const boostFactor = this.computeBoostFactor(priority);
|
|
2224
2403
|
const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
|
|
@@ -2385,6 +2564,19 @@ var init_srs = __esm({
|
|
|
2385
2564
|
}
|
|
2386
2565
|
});
|
|
2387
2566
|
|
|
2567
|
+
// src/core/navigators/userGoal.ts
|
|
2568
|
+
var userGoal_exports = {};
|
|
2569
|
+
__export(userGoal_exports, {
|
|
2570
|
+
USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
|
|
2571
|
+
});
|
|
2572
|
+
var USER_GOAL_NAVIGATOR_STUB;
|
|
2573
|
+
var init_userGoal = __esm({
|
|
2574
|
+
"src/core/navigators/userGoal.ts"() {
|
|
2575
|
+
"use strict";
|
|
2576
|
+
USER_GOAL_NAVIGATOR_STUB = true;
|
|
2577
|
+
}
|
|
2578
|
+
});
|
|
2579
|
+
|
|
2388
2580
|
// import("./**/*") in src/core/navigators/index.ts
|
|
2389
2581
|
var globImport;
|
|
2390
2582
|
var init_ = __esm({
|
|
@@ -2397,14 +2589,17 @@ var init_ = __esm({
|
|
|
2397
2589
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
2398
2590
|
"./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
|
|
2399
2591
|
"./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
2592
|
+
"./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
|
|
2400
2593
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
2401
2594
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
|
|
2402
2595
|
"./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
|
|
2403
2596
|
"./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
2404
2597
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
|
|
2598
|
+
"./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
|
|
2405
2599
|
"./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
|
|
2406
2600
|
"./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
|
|
2407
|
-
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
2601
|
+
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
2602
|
+
"./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
|
|
2408
2603
|
});
|
|
2409
2604
|
}
|
|
2410
2605
|
});
|
|
@@ -2453,6 +2648,7 @@ var init_navigators = __esm({
|
|
|
2453
2648
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
2454
2649
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
2455
2650
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
2651
|
+
Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
|
|
2456
2652
|
return Navigators2;
|
|
2457
2653
|
})(Navigators || {});
|
|
2458
2654
|
NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
|
|
@@ -2466,7 +2662,8 @@ var init_navigators = __esm({
|
|
|
2466
2662
|
["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
|
|
2467
2663
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
2468
2664
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
2469
|
-
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER
|
|
2665
|
+
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
2666
|
+
["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
|
|
2470
2667
|
};
|
|
2471
2668
|
ContentNavigator = class {
|
|
2472
2669
|
/** User interface for this navigation session */
|
|
@@ -2491,6 +2688,52 @@ var init_navigators = __esm({
|
|
|
2491
2688
|
this.strategyId = strategyData._id;
|
|
2492
2689
|
}
|
|
2493
2690
|
}
|
|
2691
|
+
// ============================================================================
|
|
2692
|
+
// STRATEGY STATE HELPERS
|
|
2693
|
+
// ============================================================================
|
|
2694
|
+
//
|
|
2695
|
+
// These methods allow strategies to persist their own state (user preferences,
|
|
2696
|
+
// learned patterns, temporal tracking) in the user database.
|
|
2697
|
+
//
|
|
2698
|
+
// ============================================================================
|
|
2699
|
+
/**
|
|
2700
|
+
* Unique key identifying this strategy for state storage.
|
|
2701
|
+
*
|
|
2702
|
+
* Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
|
|
2703
|
+
* Override in subclasses if multiple instances of the same strategy type
|
|
2704
|
+
* need separate state storage.
|
|
2705
|
+
*/
|
|
2706
|
+
get strategyKey() {
|
|
2707
|
+
return this.constructor.name;
|
|
2708
|
+
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Get this strategy's persisted state for the current course.
|
|
2711
|
+
*
|
|
2712
|
+
* @returns The strategy's data payload, or null if no state exists
|
|
2713
|
+
* @throws Error if user or course is not initialized
|
|
2714
|
+
*/
|
|
2715
|
+
async getStrategyState() {
|
|
2716
|
+
if (!this.user || !this.course) {
|
|
2717
|
+
throw new Error(
|
|
2718
|
+
`Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2719
|
+
);
|
|
2720
|
+
}
|
|
2721
|
+
return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
|
|
2722
|
+
}
|
|
2723
|
+
/**
|
|
2724
|
+
* Persist this strategy's state for the current course.
|
|
2725
|
+
*
|
|
2726
|
+
* @param data - The strategy's data payload to store
|
|
2727
|
+
* @throws Error if user or course is not initialized
|
|
2728
|
+
*/
|
|
2729
|
+
async putStrategyState(data) {
|
|
2730
|
+
if (!this.user || !this.course) {
|
|
2731
|
+
throw new Error(
|
|
2732
|
+
`Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
|
|
2736
|
+
}
|
|
2494
2737
|
/**
|
|
2495
2738
|
* Factory method to create navigator instances dynamically.
|
|
2496
2739
|
*
|
|
@@ -2865,15 +3108,6 @@ var init_courseDB = __esm({
|
|
|
2865
3108
|
ret[r.id] = r.doc.id_displayable_data;
|
|
2866
3109
|
}
|
|
2867
3110
|
});
|
|
2868
|
-
await Promise.all(
|
|
2869
|
-
cards.rows.map((r) => {
|
|
2870
|
-
return async () => {
|
|
2871
|
-
if (isSuccessRow(r)) {
|
|
2872
|
-
ret[r.id] = r.doc.id_displayable_data;
|
|
2873
|
-
}
|
|
2874
|
-
};
|
|
2875
|
-
})
|
|
2876
|
-
);
|
|
2877
3111
|
return ret;
|
|
2878
3112
|
}
|
|
2879
3113
|
async getCardsByELO(elo, cardLimit) {
|
|
@@ -2958,6 +3192,28 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
2958
3192
|
throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
|
|
2959
3193
|
}
|
|
2960
3194
|
}
|
|
3195
|
+
async getAppliedTagsBatch(cardIds) {
|
|
3196
|
+
if (cardIds.length === 0) {
|
|
3197
|
+
return /* @__PURE__ */ new Map();
|
|
3198
|
+
}
|
|
3199
|
+
const db = getCourseDB2(this.id);
|
|
3200
|
+
const result = await db.query("getTags", {
|
|
3201
|
+
keys: cardIds,
|
|
3202
|
+
include_docs: false
|
|
3203
|
+
});
|
|
3204
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
3205
|
+
for (const cardId of cardIds) {
|
|
3206
|
+
tagsByCard.set(cardId, []);
|
|
3207
|
+
}
|
|
3208
|
+
for (const row of result.rows) {
|
|
3209
|
+
const cardId = row.key;
|
|
3210
|
+
const tagName = row.value?.name;
|
|
3211
|
+
if (tagName && tagsByCard.has(cardId)) {
|
|
3212
|
+
tagsByCard.get(cardId).push(tagName);
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
return tagsByCard;
|
|
3216
|
+
}
|
|
2961
3217
|
async addTagToCard(cardId, tagId, updateELO) {
|
|
2962
3218
|
return await addTagToCard(
|
|
2963
3219
|
this.id,
|
|
@@ -3659,8 +3915,7 @@ var init_adminDB2 = __esm({
|
|
|
3659
3915
|
}
|
|
3660
3916
|
}
|
|
3661
3917
|
}
|
|
3662
|
-
|
|
3663
|
-
return dbs.map((db) => {
|
|
3918
|
+
return promisedCRDbs.map((db) => {
|
|
3664
3919
|
return {
|
|
3665
3920
|
...db.getConfig(),
|
|
3666
3921
|
_id: db._id
|
|
@@ -4033,7 +4288,9 @@ import moment6 from "moment";
|
|
|
4033
4288
|
function accomodateGuest() {
|
|
4034
4289
|
logger.log("[funnel] accomodateGuest() called");
|
|
4035
4290
|
if (typeof localStorage === "undefined") {
|
|
4036
|
-
logger.log(
|
|
4291
|
+
logger.log(
|
|
4292
|
+
"[funnel] localStorage not available (Node.js environment), returning default guest"
|
|
4293
|
+
);
|
|
4037
4294
|
return {
|
|
4038
4295
|
username: GuestUsername + "nodejs-test",
|
|
4039
4296
|
firstVisit: true
|
|
@@ -5011,6 +5268,55 @@ Currently logged-in as ${this._username}.`
|
|
|
5011
5268
|
async updateUserElo(courseId, elo) {
|
|
5012
5269
|
return updateUserElo(this._username, courseId, elo);
|
|
5013
5270
|
}
|
|
5271
|
+
async getStrategyState(courseId, strategyKey) {
|
|
5272
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
5273
|
+
try {
|
|
5274
|
+
const doc = await this.localDB.get(docId);
|
|
5275
|
+
return doc.data;
|
|
5276
|
+
} catch (e) {
|
|
5277
|
+
const err = e;
|
|
5278
|
+
if (err.status === 404) {
|
|
5279
|
+
return null;
|
|
5280
|
+
}
|
|
5281
|
+
throw e;
|
|
5282
|
+
}
|
|
5283
|
+
}
|
|
5284
|
+
async putStrategyState(courseId, strategyKey, data) {
|
|
5285
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
5286
|
+
let existingRev;
|
|
5287
|
+
try {
|
|
5288
|
+
const existing = await this.localDB.get(docId);
|
|
5289
|
+
existingRev = existing._rev;
|
|
5290
|
+
} catch (e) {
|
|
5291
|
+
const err = e;
|
|
5292
|
+
if (err.status !== 404) {
|
|
5293
|
+
throw e;
|
|
5294
|
+
}
|
|
5295
|
+
}
|
|
5296
|
+
const doc = {
|
|
5297
|
+
_id: docId,
|
|
5298
|
+
_rev: existingRev,
|
|
5299
|
+
docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
|
|
5300
|
+
courseId,
|
|
5301
|
+
strategyKey,
|
|
5302
|
+
data,
|
|
5303
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5304
|
+
};
|
|
5305
|
+
await this.localDB.put(doc);
|
|
5306
|
+
}
|
|
5307
|
+
async deleteStrategyState(courseId, strategyKey) {
|
|
5308
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
5309
|
+
try {
|
|
5310
|
+
const doc = await this.localDB.get(docId);
|
|
5311
|
+
await this.localDB.remove(doc);
|
|
5312
|
+
} catch (e) {
|
|
5313
|
+
const err = e;
|
|
5314
|
+
if (err.status === 404) {
|
|
5315
|
+
return;
|
|
5316
|
+
}
|
|
5317
|
+
throw e;
|
|
5318
|
+
}
|
|
5319
|
+
}
|
|
5014
5320
|
};
|
|
5015
5321
|
userCoursesDoc = "CourseRegistrations";
|
|
5016
5322
|
userClassroomsDoc = "ClassroomRegistrations";
|
|
@@ -5678,6 +5984,14 @@ var init_courseDB2 = __esm({
|
|
|
5678
5984
|
};
|
|
5679
5985
|
}
|
|
5680
5986
|
}
|
|
5987
|
+
async getAppliedTagsBatch(cardIds) {
|
|
5988
|
+
const tagsIndex = await this.unpacker.getTagsIndex();
|
|
5989
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
5990
|
+
for (const cardId of cardIds) {
|
|
5991
|
+
tagsByCard.set(cardId, tagsIndex.byCard[cardId] || []);
|
|
5992
|
+
}
|
|
5993
|
+
return tagsByCard;
|
|
5994
|
+
}
|
|
5681
5995
|
async addTagToCard(_cardId, _tagId) {
|
|
5682
5996
|
throw new Error("Cannot modify tags in static mode");
|
|
5683
5997
|
}
|
|
@@ -6384,6 +6698,16 @@ var init_user = __esm({
|
|
|
6384
6698
|
}
|
|
6385
6699
|
});
|
|
6386
6700
|
|
|
6701
|
+
// src/core/types/strategyState.ts
|
|
6702
|
+
function buildStrategyStateId(courseId, strategyKey) {
|
|
6703
|
+
return `STRATEGY_STATE::${courseId}::${strategyKey}`;
|
|
6704
|
+
}
|
|
6705
|
+
var init_strategyState = __esm({
|
|
6706
|
+
"src/core/types/strategyState.ts"() {
|
|
6707
|
+
"use strict";
|
|
6708
|
+
}
|
|
6709
|
+
});
|
|
6710
|
+
|
|
6387
6711
|
// src/core/bulkImport/cardProcessor.ts
|
|
6388
6712
|
import { Status as Status5 } from "@vue-skuilder/common";
|
|
6389
6713
|
async function importParsedCards(parsedCards, courseDB, config) {
|
|
@@ -6527,6 +6851,7 @@ var init_core = __esm({
|
|
|
6527
6851
|
init_interfaces();
|
|
6528
6852
|
init_types_legacy();
|
|
6529
6853
|
init_user();
|
|
6854
|
+
init_strategyState();
|
|
6530
6855
|
init_Loggable();
|
|
6531
6856
|
init_util();
|
|
6532
6857
|
init_navigators();
|
|
@@ -8971,6 +9296,7 @@ export {
|
|
|
8971
9296
|
TagFilteredContentSource,
|
|
8972
9297
|
_resetDataLayer,
|
|
8973
9298
|
areQuestionRecords,
|
|
9299
|
+
buildStrategyStateId,
|
|
8974
9300
|
docIsDeleted,
|
|
8975
9301
|
ensureAppDataDirectory,
|
|
8976
9302
|
getAppDataDirectory,
|