@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.js
CHANGED
|
@@ -122,6 +122,7 @@ var init_types_legacy = __esm({
|
|
|
122
122
|
DocType3["SCHEDULED_CARD"] = "SCHEDULED_CARD";
|
|
123
123
|
DocType3["TAG"] = "TAG";
|
|
124
124
|
DocType3["NAVIGATION_STRATEGY"] = "NAVIGATION_STRATEGY";
|
|
125
|
+
DocType3["STRATEGY_STATE"] = "STRATEGY_STATE";
|
|
125
126
|
return DocType3;
|
|
126
127
|
})(DocType || {});
|
|
127
128
|
DocTypePrefixes = {
|
|
@@ -135,7 +136,8 @@ var init_types_legacy = __esm({
|
|
|
135
136
|
["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
|
|
136
137
|
["VIEW" /* VIEW */]: "VIEW",
|
|
137
138
|
["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
|
|
138
|
-
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
|
|
139
|
+
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY",
|
|
140
|
+
["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE"
|
|
139
141
|
};
|
|
140
142
|
}
|
|
141
143
|
});
|
|
@@ -1134,6 +1136,41 @@ var Pipeline_exports = {};
|
|
|
1134
1136
|
__export(Pipeline_exports, {
|
|
1135
1137
|
Pipeline: () => Pipeline
|
|
1136
1138
|
});
|
|
1139
|
+
function logPipelineConfig(generator, filters) {
|
|
1140
|
+
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
1141
|
+
logger.info(
|
|
1142
|
+
`[Pipeline] Configuration:
|
|
1143
|
+
Generator: ${generator.name}
|
|
1144
|
+
Filters:${filterList}`
|
|
1145
|
+
);
|
|
1146
|
+
}
|
|
1147
|
+
function logTagHydration(cards, tagsByCard) {
|
|
1148
|
+
const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
|
|
1149
|
+
const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
|
|
1150
|
+
logger.debug(
|
|
1151
|
+
`[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
|
|
1152
|
+
);
|
|
1153
|
+
}
|
|
1154
|
+
function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
|
|
1155
|
+
const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
|
|
1156
|
+
logger.info(
|
|
1157
|
+
`[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
function logCardProvenance(cards, maxCards = 3) {
|
|
1161
|
+
const cardsToLog = cards.slice(0, maxCards);
|
|
1162
|
+
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
1163
|
+
for (const card of cardsToLog) {
|
|
1164
|
+
logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
|
|
1165
|
+
for (const entry of card.provenance) {
|
|
1166
|
+
const scoreChange = entry.score.toFixed(3);
|
|
1167
|
+
const action = entry.action.padEnd(9);
|
|
1168
|
+
logger.debug(
|
|
1169
|
+
`[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
|
|
1170
|
+
);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1137
1174
|
var import_common5, Pipeline;
|
|
1138
1175
|
var init_Pipeline = __esm({
|
|
1139
1176
|
"src/core/navigators/Pipeline.ts"() {
|
|
@@ -1158,19 +1195,18 @@ var init_Pipeline = __esm({
|
|
|
1158
1195
|
this.filters = filters;
|
|
1159
1196
|
this.user = user;
|
|
1160
1197
|
this.course = course;
|
|
1161
|
-
|
|
1162
|
-
`[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
|
|
1163
|
-
);
|
|
1198
|
+
logPipelineConfig(generator, filters);
|
|
1164
1199
|
}
|
|
1165
1200
|
/**
|
|
1166
1201
|
* Get weighted cards by running generator and applying filters.
|
|
1167
1202
|
*
|
|
1168
1203
|
* 1. Build shared context (user ELO, etc.)
|
|
1169
1204
|
* 2. Get candidates from generator (passing context)
|
|
1170
|
-
* 3.
|
|
1171
|
-
* 4.
|
|
1172
|
-
* 5.
|
|
1173
|
-
* 6.
|
|
1205
|
+
* 3. Batch hydrate tags for all candidates
|
|
1206
|
+
* 4. Apply each filter sequentially
|
|
1207
|
+
* 5. Remove zero-score cards
|
|
1208
|
+
* 6. Sort by score descending
|
|
1209
|
+
* 7. Return top N
|
|
1174
1210
|
*
|
|
1175
1211
|
* @param limit - Maximum number of cards to return
|
|
1176
1212
|
* @returns Cards sorted by score descending
|
|
@@ -1183,7 +1219,9 @@ var init_Pipeline = __esm({
|
|
|
1183
1219
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
1184
1220
|
);
|
|
1185
1221
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
1186
|
-
|
|
1222
|
+
const generatedCount = cards.length;
|
|
1223
|
+
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
1224
|
+
cards = await this.hydrateTags(cards);
|
|
1187
1225
|
for (const filter of this.filters) {
|
|
1188
1226
|
const beforeCount = cards.length;
|
|
1189
1227
|
cards = await filter.transform(cards, context);
|
|
@@ -1192,11 +1230,33 @@ var init_Pipeline = __esm({
|
|
|
1192
1230
|
cards = cards.filter((c) => c.score > 0);
|
|
1193
1231
|
cards.sort((a, b) => b.score - a.score);
|
|
1194
1232
|
const result = cards.slice(0, limit);
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
);
|
|
1233
|
+
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
1234
|
+
logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
|
|
1235
|
+
logCardProvenance(result, 3);
|
|
1198
1236
|
return result;
|
|
1199
1237
|
}
|
|
1238
|
+
/**
|
|
1239
|
+
* Batch hydrate tags for all cards.
|
|
1240
|
+
*
|
|
1241
|
+
* Fetches tags for all cards in a single database query and attaches them
|
|
1242
|
+
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
1243
|
+
* making individual getAppliedTags() calls.
|
|
1244
|
+
*
|
|
1245
|
+
* @param cards - Cards to hydrate
|
|
1246
|
+
* @returns Cards with tags populated
|
|
1247
|
+
*/
|
|
1248
|
+
async hydrateTags(cards) {
|
|
1249
|
+
if (cards.length === 0) {
|
|
1250
|
+
return cards;
|
|
1251
|
+
}
|
|
1252
|
+
const cardIds = cards.map((c) => c.cardId);
|
|
1253
|
+
const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
|
|
1254
|
+
logTagHydration(cards, tagsByCard);
|
|
1255
|
+
return cards.map((card) => ({
|
|
1256
|
+
...card,
|
|
1257
|
+
tags: tagsByCard.get(card.cardId) ?? []
|
|
1258
|
+
}));
|
|
1259
|
+
}
|
|
1200
1260
|
/**
|
|
1201
1261
|
* Build shared context for generator and filters.
|
|
1202
1262
|
*
|
|
@@ -1556,15 +1616,144 @@ var init_eloDistance = __esm({
|
|
|
1556
1616
|
}
|
|
1557
1617
|
});
|
|
1558
1618
|
|
|
1619
|
+
// src/core/navigators/filters/userTagPreference.ts
|
|
1620
|
+
var userTagPreference_exports = {};
|
|
1621
|
+
__export(userTagPreference_exports, {
|
|
1622
|
+
default: () => UserTagPreferenceFilter
|
|
1623
|
+
});
|
|
1624
|
+
var UserTagPreferenceFilter;
|
|
1625
|
+
var init_userTagPreference = __esm({
|
|
1626
|
+
"src/core/navigators/filters/userTagPreference.ts"() {
|
|
1627
|
+
"use strict";
|
|
1628
|
+
init_navigators();
|
|
1629
|
+
UserTagPreferenceFilter = class extends ContentNavigator {
|
|
1630
|
+
_strategyData;
|
|
1631
|
+
/** Human-readable name for CardFilter interface */
|
|
1632
|
+
name;
|
|
1633
|
+
constructor(user, course, strategyData) {
|
|
1634
|
+
super(user, course, strategyData);
|
|
1635
|
+
this._strategyData = strategyData;
|
|
1636
|
+
this.name = strategyData.name || "User Tag Preferences";
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Compute multiplier for a card based on its tags and user preferences.
|
|
1640
|
+
* Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
|
|
1641
|
+
*/
|
|
1642
|
+
computeMultiplier(cardTags, boostMap) {
|
|
1643
|
+
const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
|
|
1644
|
+
if (multipliers.length === 0) {
|
|
1645
|
+
return 1;
|
|
1646
|
+
}
|
|
1647
|
+
return Math.max(...multipliers);
|
|
1648
|
+
}
|
|
1649
|
+
/**
|
|
1650
|
+
* Build human-readable reason for the filter's decision.
|
|
1651
|
+
*/
|
|
1652
|
+
buildReason(cardTags, boostMap, multiplier) {
|
|
1653
|
+
const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
|
|
1654
|
+
if (multiplier === 0) {
|
|
1655
|
+
return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
|
|
1656
|
+
}
|
|
1657
|
+
if (multiplier < 1) {
|
|
1658
|
+
return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1659
|
+
}
|
|
1660
|
+
if (multiplier > 1) {
|
|
1661
|
+
return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1662
|
+
}
|
|
1663
|
+
return "No matching user preferences";
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* CardFilter.transform implementation.
|
|
1667
|
+
*
|
|
1668
|
+
* Apply user tag preferences:
|
|
1669
|
+
* 1. Read preferences from strategy state
|
|
1670
|
+
* 2. If no preferences, pass through unchanged
|
|
1671
|
+
* 3. For each card:
|
|
1672
|
+
* - Look up tag in boost record
|
|
1673
|
+
* - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
|
|
1674
|
+
* - If multiple tags match: use max multiplier
|
|
1675
|
+
* - Append provenance with clear reason
|
|
1676
|
+
*/
|
|
1677
|
+
async transform(cards, _context) {
|
|
1678
|
+
const prefs = await this.getStrategyState();
|
|
1679
|
+
if (!prefs || Object.keys(prefs.boost).length === 0) {
|
|
1680
|
+
return cards.map((card) => ({
|
|
1681
|
+
...card,
|
|
1682
|
+
provenance: [
|
|
1683
|
+
...card.provenance,
|
|
1684
|
+
{
|
|
1685
|
+
strategy: "userTagPreference",
|
|
1686
|
+
strategyName: this.strategyName || this.name,
|
|
1687
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1688
|
+
action: "passed",
|
|
1689
|
+
score: card.score,
|
|
1690
|
+
reason: "No user tag preferences configured"
|
|
1691
|
+
}
|
|
1692
|
+
]
|
|
1693
|
+
}));
|
|
1694
|
+
}
|
|
1695
|
+
const adjusted = await Promise.all(
|
|
1696
|
+
cards.map(async (card) => {
|
|
1697
|
+
const cardTags = card.tags ?? [];
|
|
1698
|
+
const multiplier = this.computeMultiplier(cardTags, prefs.boost);
|
|
1699
|
+
const finalScore = Math.min(1, card.score * multiplier);
|
|
1700
|
+
let action;
|
|
1701
|
+
if (multiplier === 0 || multiplier < 1) {
|
|
1702
|
+
action = "penalized";
|
|
1703
|
+
} else if (multiplier > 1) {
|
|
1704
|
+
action = "boosted";
|
|
1705
|
+
} else {
|
|
1706
|
+
action = "passed";
|
|
1707
|
+
}
|
|
1708
|
+
return {
|
|
1709
|
+
...card,
|
|
1710
|
+
score: finalScore,
|
|
1711
|
+
provenance: [
|
|
1712
|
+
...card.provenance,
|
|
1713
|
+
{
|
|
1714
|
+
strategy: "userTagPreference",
|
|
1715
|
+
strategyName: this.strategyName || this.name,
|
|
1716
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1717
|
+
action,
|
|
1718
|
+
score: finalScore,
|
|
1719
|
+
reason: this.buildReason(cardTags, prefs.boost, multiplier)
|
|
1720
|
+
}
|
|
1721
|
+
]
|
|
1722
|
+
};
|
|
1723
|
+
})
|
|
1724
|
+
);
|
|
1725
|
+
return adjusted;
|
|
1726
|
+
}
|
|
1727
|
+
/**
|
|
1728
|
+
* Legacy getWeightedCards - throws as filters should not be used as generators.
|
|
1729
|
+
*/
|
|
1730
|
+
async getWeightedCards(_limit) {
|
|
1731
|
+
throw new Error(
|
|
1732
|
+
"UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
// Legacy methods - stub implementations since filters don't generate cards
|
|
1736
|
+
async getNewCards(_n) {
|
|
1737
|
+
return [];
|
|
1738
|
+
}
|
|
1739
|
+
async getPendingReviews() {
|
|
1740
|
+
return [];
|
|
1741
|
+
}
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
});
|
|
1745
|
+
|
|
1559
1746
|
// src/core/navigators/filters/index.ts
|
|
1560
1747
|
var filters_exports = {};
|
|
1561
1748
|
__export(filters_exports, {
|
|
1749
|
+
UserTagPreferenceFilter: () => UserTagPreferenceFilter,
|
|
1562
1750
|
createEloDistanceFilter: () => createEloDistanceFilter
|
|
1563
1751
|
});
|
|
1564
1752
|
var init_filters = __esm({
|
|
1565
1753
|
"src/core/navigators/filters/index.ts"() {
|
|
1566
1754
|
"use strict";
|
|
1567
1755
|
init_eloDistance();
|
|
1756
|
+
init_userTagPreference();
|
|
1568
1757
|
}
|
|
1569
1758
|
});
|
|
1570
1759
|
|
|
@@ -1797,10 +1986,9 @@ var init_hierarchyDefinition = __esm({
|
|
|
1797
1986
|
/**
|
|
1798
1987
|
* Check if a card is unlocked and generate reason.
|
|
1799
1988
|
*/
|
|
1800
|
-
async checkCardUnlock(
|
|
1989
|
+
async checkCardUnlock(card, course, unlockedTags, masteredTags) {
|
|
1801
1990
|
try {
|
|
1802
|
-
const
|
|
1803
|
-
const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
|
|
1991
|
+
const cardTags = card.tags ?? [];
|
|
1804
1992
|
const lockedTags = cardTags.filter(
|
|
1805
1993
|
(tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
|
|
1806
1994
|
);
|
|
@@ -1837,7 +2025,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1837
2025
|
const gated = [];
|
|
1838
2026
|
for (const card of cards) {
|
|
1839
2027
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
1840
|
-
card
|
|
2028
|
+
card,
|
|
1841
2029
|
context.course,
|
|
1842
2030
|
unlockedTags,
|
|
1843
2031
|
masteredTags
|
|
@@ -1883,6 +2071,19 @@ var init_hierarchyDefinition = __esm({
|
|
|
1883
2071
|
}
|
|
1884
2072
|
});
|
|
1885
2073
|
|
|
2074
|
+
// src/core/navigators/inferredPreference.ts
|
|
2075
|
+
var inferredPreference_exports = {};
|
|
2076
|
+
__export(inferredPreference_exports, {
|
|
2077
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
|
|
2078
|
+
});
|
|
2079
|
+
var INFERRED_PREFERENCE_NAVIGATOR_STUB;
|
|
2080
|
+
var init_inferredPreference = __esm({
|
|
2081
|
+
"src/core/navigators/inferredPreference.ts"() {
|
|
2082
|
+
"use strict";
|
|
2083
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
|
|
1886
2087
|
// src/core/navigators/interferenceMitigator.ts
|
|
1887
2088
|
var interferenceMitigator_exports = {};
|
|
1888
2089
|
__export(interferenceMitigator_exports, {
|
|
@@ -2014,17 +2215,6 @@ var init_interferenceMitigator = __esm({
|
|
|
2014
2215
|
}
|
|
2015
2216
|
return avoid;
|
|
2016
2217
|
}
|
|
2017
|
-
/**
|
|
2018
|
-
* Get tags for a single card
|
|
2019
|
-
*/
|
|
2020
|
-
async getCardTags(cardId, course) {
|
|
2021
|
-
try {
|
|
2022
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
2023
|
-
return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
|
|
2024
|
-
} catch {
|
|
2025
|
-
return [];
|
|
2026
|
-
}
|
|
2027
|
-
}
|
|
2028
2218
|
/**
|
|
2029
2219
|
* Compute interference score reduction for a card.
|
|
2030
2220
|
* Returns: { multiplier, interfering tags, reason }
|
|
@@ -2076,7 +2266,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2076
2266
|
const tagsToAvoid = this.getTagsToAvoid(immatureTags);
|
|
2077
2267
|
const adjusted = [];
|
|
2078
2268
|
for (const card of cards) {
|
|
2079
|
-
const cardTags =
|
|
2269
|
+
const cardTags = card.tags ?? [];
|
|
2080
2270
|
const { multiplier, reason } = this.computeInterferenceEffect(
|
|
2081
2271
|
cardTags,
|
|
2082
2272
|
tagsToAvoid,
|
|
@@ -2221,27 +2411,16 @@ var init_relativePriority = __esm({
|
|
|
2221
2411
|
return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
|
|
2222
2412
|
}
|
|
2223
2413
|
}
|
|
2224
|
-
/**
|
|
2225
|
-
* Get tags for a single card.
|
|
2226
|
-
*/
|
|
2227
|
-
async getCardTags(cardId, course) {
|
|
2228
|
-
try {
|
|
2229
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
2230
|
-
return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
|
|
2231
|
-
} catch {
|
|
2232
|
-
return [];
|
|
2233
|
-
}
|
|
2234
|
-
}
|
|
2235
2414
|
/**
|
|
2236
2415
|
* CardFilter.transform implementation.
|
|
2237
2416
|
*
|
|
2238
2417
|
* Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
|
|
2239
2418
|
* cards with low-priority tags get reduced scores.
|
|
2240
2419
|
*/
|
|
2241
|
-
async transform(cards,
|
|
2420
|
+
async transform(cards, _context) {
|
|
2242
2421
|
const adjusted = await Promise.all(
|
|
2243
2422
|
cards.map(async (card) => {
|
|
2244
|
-
const cardTags =
|
|
2423
|
+
const cardTags = card.tags ?? [];
|
|
2245
2424
|
const priority = this.computeCardPriority(cardTags);
|
|
2246
2425
|
const boostFactor = this.computeBoostFactor(priority);
|
|
2247
2426
|
const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
|
|
@@ -2408,6 +2587,19 @@ var init_srs = __esm({
|
|
|
2408
2587
|
}
|
|
2409
2588
|
});
|
|
2410
2589
|
|
|
2590
|
+
// src/core/navigators/userGoal.ts
|
|
2591
|
+
var userGoal_exports = {};
|
|
2592
|
+
__export(userGoal_exports, {
|
|
2593
|
+
USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
|
|
2594
|
+
});
|
|
2595
|
+
var USER_GOAL_NAVIGATOR_STUB;
|
|
2596
|
+
var init_userGoal = __esm({
|
|
2597
|
+
"src/core/navigators/userGoal.ts"() {
|
|
2598
|
+
"use strict";
|
|
2599
|
+
USER_GOAL_NAVIGATOR_STUB = true;
|
|
2600
|
+
}
|
|
2601
|
+
});
|
|
2602
|
+
|
|
2411
2603
|
// import("./**/*") in src/core/navigators/index.ts
|
|
2412
2604
|
var globImport;
|
|
2413
2605
|
var init_ = __esm({
|
|
@@ -2420,14 +2612,17 @@ var init_ = __esm({
|
|
|
2420
2612
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
2421
2613
|
"./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
|
|
2422
2614
|
"./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
2615
|
+
"./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
|
|
2423
2616
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
2424
2617
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
|
|
2425
2618
|
"./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
|
|
2426
2619
|
"./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
2427
2620
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
|
|
2621
|
+
"./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
|
|
2428
2622
|
"./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
|
|
2429
2623
|
"./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
|
|
2430
|
-
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
2624
|
+
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
2625
|
+
"./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
|
|
2431
2626
|
});
|
|
2432
2627
|
}
|
|
2433
2628
|
});
|
|
@@ -2476,6 +2671,7 @@ var init_navigators = __esm({
|
|
|
2476
2671
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
2477
2672
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
2478
2673
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
2674
|
+
Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
|
|
2479
2675
|
return Navigators2;
|
|
2480
2676
|
})(Navigators || {});
|
|
2481
2677
|
NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
|
|
@@ -2489,7 +2685,8 @@ var init_navigators = __esm({
|
|
|
2489
2685
|
["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
|
|
2490
2686
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
2491
2687
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
2492
|
-
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER
|
|
2688
|
+
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
2689
|
+
["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
|
|
2493
2690
|
};
|
|
2494
2691
|
ContentNavigator = class {
|
|
2495
2692
|
/** User interface for this navigation session */
|
|
@@ -2514,6 +2711,52 @@ var init_navigators = __esm({
|
|
|
2514
2711
|
this.strategyId = strategyData._id;
|
|
2515
2712
|
}
|
|
2516
2713
|
}
|
|
2714
|
+
// ============================================================================
|
|
2715
|
+
// STRATEGY STATE HELPERS
|
|
2716
|
+
// ============================================================================
|
|
2717
|
+
//
|
|
2718
|
+
// These methods allow strategies to persist their own state (user preferences,
|
|
2719
|
+
// learned patterns, temporal tracking) in the user database.
|
|
2720
|
+
//
|
|
2721
|
+
// ============================================================================
|
|
2722
|
+
/**
|
|
2723
|
+
* Unique key identifying this strategy for state storage.
|
|
2724
|
+
*
|
|
2725
|
+
* Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
|
|
2726
|
+
* Override in subclasses if multiple instances of the same strategy type
|
|
2727
|
+
* need separate state storage.
|
|
2728
|
+
*/
|
|
2729
|
+
get strategyKey() {
|
|
2730
|
+
return this.constructor.name;
|
|
2731
|
+
}
|
|
2732
|
+
/**
|
|
2733
|
+
* Get this strategy's persisted state for the current course.
|
|
2734
|
+
*
|
|
2735
|
+
* @returns The strategy's data payload, or null if no state exists
|
|
2736
|
+
* @throws Error if user or course is not initialized
|
|
2737
|
+
*/
|
|
2738
|
+
async getStrategyState() {
|
|
2739
|
+
if (!this.user || !this.course) {
|
|
2740
|
+
throw new Error(
|
|
2741
|
+
`Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2742
|
+
);
|
|
2743
|
+
}
|
|
2744
|
+
return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Persist this strategy's state for the current course.
|
|
2748
|
+
*
|
|
2749
|
+
* @param data - The strategy's data payload to store
|
|
2750
|
+
* @throws Error if user or course is not initialized
|
|
2751
|
+
*/
|
|
2752
|
+
async putStrategyState(data) {
|
|
2753
|
+
if (!this.user || !this.course) {
|
|
2754
|
+
throw new Error(
|
|
2755
|
+
`Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2756
|
+
);
|
|
2757
|
+
}
|
|
2758
|
+
return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
|
|
2759
|
+
}
|
|
2517
2760
|
/**
|
|
2518
2761
|
* Factory method to create navigator instances dynamically.
|
|
2519
2762
|
*
|
|
@@ -2883,15 +3126,6 @@ var init_courseDB = __esm({
|
|
|
2883
3126
|
ret[r.id] = r.doc.id_displayable_data;
|
|
2884
3127
|
}
|
|
2885
3128
|
});
|
|
2886
|
-
await Promise.all(
|
|
2887
|
-
cards.rows.map((r) => {
|
|
2888
|
-
return async () => {
|
|
2889
|
-
if (isSuccessRow(r)) {
|
|
2890
|
-
ret[r.id] = r.doc.id_displayable_data;
|
|
2891
|
-
}
|
|
2892
|
-
};
|
|
2893
|
-
})
|
|
2894
|
-
);
|
|
2895
3129
|
return ret;
|
|
2896
3130
|
}
|
|
2897
3131
|
async getCardsByELO(elo, cardLimit) {
|
|
@@ -2976,6 +3210,28 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
2976
3210
|
throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
|
|
2977
3211
|
}
|
|
2978
3212
|
}
|
|
3213
|
+
async getAppliedTagsBatch(cardIds) {
|
|
3214
|
+
if (cardIds.length === 0) {
|
|
3215
|
+
return /* @__PURE__ */ new Map();
|
|
3216
|
+
}
|
|
3217
|
+
const db = getCourseDB2(this.id);
|
|
3218
|
+
const result = await db.query("getTags", {
|
|
3219
|
+
keys: cardIds,
|
|
3220
|
+
include_docs: false
|
|
3221
|
+
});
|
|
3222
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
3223
|
+
for (const cardId of cardIds) {
|
|
3224
|
+
tagsByCard.set(cardId, []);
|
|
3225
|
+
}
|
|
3226
|
+
for (const row of result.rows) {
|
|
3227
|
+
const cardId = row.key;
|
|
3228
|
+
const tagName = row.value?.name;
|
|
3229
|
+
if (tagName && tagsByCard.has(cardId)) {
|
|
3230
|
+
tagsByCard.get(cardId).push(tagName);
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
return tagsByCard;
|
|
3234
|
+
}
|
|
2979
3235
|
async addTagToCard(cardId, tagId, updateELO) {
|
|
2980
3236
|
return await addTagToCard(
|
|
2981
3237
|
this.id,
|
|
@@ -3677,8 +3933,7 @@ var init_adminDB2 = __esm({
|
|
|
3677
3933
|
}
|
|
3678
3934
|
}
|
|
3679
3935
|
}
|
|
3680
|
-
|
|
3681
|
-
return dbs.map((db) => {
|
|
3936
|
+
return promisedCRDbs.map((db) => {
|
|
3682
3937
|
return {
|
|
3683
3938
|
...db.getConfig(),
|
|
3684
3939
|
_id: db._id
|
|
@@ -4050,7 +4305,9 @@ var init_couch = __esm({
|
|
|
4050
4305
|
function accomodateGuest() {
|
|
4051
4306
|
logger.log("[funnel] accomodateGuest() called");
|
|
4052
4307
|
if (typeof localStorage === "undefined") {
|
|
4053
|
-
logger.log(
|
|
4308
|
+
logger.log(
|
|
4309
|
+
"[funnel] localStorage not available (Node.js environment), returning default guest"
|
|
4310
|
+
);
|
|
4054
4311
|
return {
|
|
4055
4312
|
username: GuestUsername + "nodejs-test",
|
|
4056
4313
|
firstVisit: true
|
|
@@ -5030,6 +5287,55 @@ Currently logged-in as ${this._username}.`
|
|
|
5030
5287
|
async updateUserElo(courseId, elo) {
|
|
5031
5288
|
return updateUserElo(this._username, courseId, elo);
|
|
5032
5289
|
}
|
|
5290
|
+
async getStrategyState(courseId, strategyKey) {
|
|
5291
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
5292
|
+
try {
|
|
5293
|
+
const doc = await this.localDB.get(docId);
|
|
5294
|
+
return doc.data;
|
|
5295
|
+
} catch (e) {
|
|
5296
|
+
const err = e;
|
|
5297
|
+
if (err.status === 404) {
|
|
5298
|
+
return null;
|
|
5299
|
+
}
|
|
5300
|
+
throw e;
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
async putStrategyState(courseId, strategyKey, data) {
|
|
5304
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
5305
|
+
let existingRev;
|
|
5306
|
+
try {
|
|
5307
|
+
const existing = await this.localDB.get(docId);
|
|
5308
|
+
existingRev = existing._rev;
|
|
5309
|
+
} catch (e) {
|
|
5310
|
+
const err = e;
|
|
5311
|
+
if (err.status !== 404) {
|
|
5312
|
+
throw e;
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
5315
|
+
const doc = {
|
|
5316
|
+
_id: docId,
|
|
5317
|
+
_rev: existingRev,
|
|
5318
|
+
docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
|
|
5319
|
+
courseId,
|
|
5320
|
+
strategyKey,
|
|
5321
|
+
data,
|
|
5322
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5323
|
+
};
|
|
5324
|
+
await this.localDB.put(doc);
|
|
5325
|
+
}
|
|
5326
|
+
async deleteStrategyState(courseId, strategyKey) {
|
|
5327
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
5328
|
+
try {
|
|
5329
|
+
const doc = await this.localDB.get(docId);
|
|
5330
|
+
await this.localDB.remove(doc);
|
|
5331
|
+
} catch (e) {
|
|
5332
|
+
const err = e;
|
|
5333
|
+
if (err.status === 404) {
|
|
5334
|
+
return;
|
|
5335
|
+
}
|
|
5336
|
+
throw e;
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5033
5339
|
};
|
|
5034
5340
|
userCoursesDoc = "CourseRegistrations";
|
|
5035
5341
|
userClassroomsDoc = "ClassroomRegistrations";
|
|
@@ -5697,6 +6003,14 @@ var init_courseDB2 = __esm({
|
|
|
5697
6003
|
};
|
|
5698
6004
|
}
|
|
5699
6005
|
}
|
|
6006
|
+
async getAppliedTagsBatch(cardIds) {
|
|
6007
|
+
const tagsIndex = await this.unpacker.getTagsIndex();
|
|
6008
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
6009
|
+
for (const cardId of cardIds) {
|
|
6010
|
+
tagsByCard.set(cardId, tagsIndex.byCard[cardId] || []);
|
|
6011
|
+
}
|
|
6012
|
+
return tagsByCard;
|
|
6013
|
+
}
|
|
5700
6014
|
async addTagToCard(_cardId, _tagId) {
|
|
5701
6015
|
throw new Error("Cannot modify tags in static mode");
|
|
5702
6016
|
}
|
|
@@ -6404,6 +6718,16 @@ var init_user = __esm({
|
|
|
6404
6718
|
}
|
|
6405
6719
|
});
|
|
6406
6720
|
|
|
6721
|
+
// src/core/types/strategyState.ts
|
|
6722
|
+
function buildStrategyStateId(courseId, strategyKey) {
|
|
6723
|
+
return `STRATEGY_STATE::${courseId}::${strategyKey}`;
|
|
6724
|
+
}
|
|
6725
|
+
var init_strategyState = __esm({
|
|
6726
|
+
"src/core/types/strategyState.ts"() {
|
|
6727
|
+
"use strict";
|
|
6728
|
+
}
|
|
6729
|
+
});
|
|
6730
|
+
|
|
6407
6731
|
// src/core/bulkImport/cardProcessor.ts
|
|
6408
6732
|
async function importParsedCards(parsedCards, courseDB, config) {
|
|
6409
6733
|
const results = [];
|
|
@@ -6548,6 +6872,7 @@ var init_core = __esm({
|
|
|
6548
6872
|
init_interfaces();
|
|
6549
6873
|
init_types_legacy();
|
|
6550
6874
|
init_user();
|
|
6875
|
+
init_strategyState();
|
|
6551
6876
|
init_Loggable();
|
|
6552
6877
|
init_util();
|
|
6553
6878
|
init_navigators();
|
|
@@ -6576,6 +6901,7 @@ __export(index_exports, {
|
|
|
6576
6901
|
TagFilteredContentSource: () => TagFilteredContentSource,
|
|
6577
6902
|
_resetDataLayer: () => _resetDataLayer,
|
|
6578
6903
|
areQuestionRecords: () => areQuestionRecords,
|
|
6904
|
+
buildStrategyStateId: () => buildStrategyStateId,
|
|
6579
6905
|
docIsDeleted: () => docIsDeleted,
|
|
6580
6906
|
ensureAppDataDirectory: () => ensureAppDataDirectory,
|
|
6581
6907
|
getAppDataDirectory: () => getAppDataDirectory,
|
|
@@ -9037,6 +9363,7 @@ init_factory();
|
|
|
9037
9363
|
TagFilteredContentSource,
|
|
9038
9364
|
_resetDataLayer,
|
|
9039
9365
|
areQuestionRecords,
|
|
9366
|
+
buildStrategyStateId,
|
|
9040
9367
|
docIsDeleted,
|
|
9041
9368
|
ensureAppDataDirectory,
|
|
9042
9369
|
getAppDataDirectory,
|