@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
|
@@ -97,7 +97,8 @@ var init_types_legacy = __esm({
|
|
|
97
97
|
["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
|
|
98
98
|
["VIEW" /* VIEW */]: "VIEW",
|
|
99
99
|
["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
|
|
100
|
-
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
|
|
100
|
+
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY",
|
|
101
|
+
["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE"
|
|
101
102
|
};
|
|
102
103
|
}
|
|
103
104
|
});
|
|
@@ -665,6 +666,41 @@ __export(Pipeline_exports, {
|
|
|
665
666
|
Pipeline: () => Pipeline
|
|
666
667
|
});
|
|
667
668
|
import { toCourseElo as toCourseElo2 } from "@vue-skuilder/common";
|
|
669
|
+
function logPipelineConfig(generator, filters) {
|
|
670
|
+
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
671
|
+
logger.info(
|
|
672
|
+
`[Pipeline] Configuration:
|
|
673
|
+
Generator: ${generator.name}
|
|
674
|
+
Filters:${filterList}`
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
function logTagHydration(cards, tagsByCard) {
|
|
678
|
+
const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
|
|
679
|
+
const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
|
|
680
|
+
logger.debug(
|
|
681
|
+
`[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
|
|
685
|
+
const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
|
|
686
|
+
logger.info(
|
|
687
|
+
`[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
function logCardProvenance(cards, maxCards = 3) {
|
|
691
|
+
const cardsToLog = cards.slice(0, maxCards);
|
|
692
|
+
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
693
|
+
for (const card of cardsToLog) {
|
|
694
|
+
logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
|
|
695
|
+
for (const entry of card.provenance) {
|
|
696
|
+
const scoreChange = entry.score.toFixed(3);
|
|
697
|
+
const action = entry.action.padEnd(9);
|
|
698
|
+
logger.debug(
|
|
699
|
+
`[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
668
704
|
var Pipeline;
|
|
669
705
|
var init_Pipeline = __esm({
|
|
670
706
|
"src/core/navigators/Pipeline.ts"() {
|
|
@@ -688,19 +724,18 @@ var init_Pipeline = __esm({
|
|
|
688
724
|
this.filters = filters;
|
|
689
725
|
this.user = user;
|
|
690
726
|
this.course = course;
|
|
691
|
-
|
|
692
|
-
`[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
|
|
693
|
-
);
|
|
727
|
+
logPipelineConfig(generator, filters);
|
|
694
728
|
}
|
|
695
729
|
/**
|
|
696
730
|
* Get weighted cards by running generator and applying filters.
|
|
697
731
|
*
|
|
698
732
|
* 1. Build shared context (user ELO, etc.)
|
|
699
733
|
* 2. Get candidates from generator (passing context)
|
|
700
|
-
* 3.
|
|
701
|
-
* 4.
|
|
702
|
-
* 5.
|
|
703
|
-
* 6.
|
|
734
|
+
* 3. Batch hydrate tags for all candidates
|
|
735
|
+
* 4. Apply each filter sequentially
|
|
736
|
+
* 5. Remove zero-score cards
|
|
737
|
+
* 6. Sort by score descending
|
|
738
|
+
* 7. Return top N
|
|
704
739
|
*
|
|
705
740
|
* @param limit - Maximum number of cards to return
|
|
706
741
|
* @returns Cards sorted by score descending
|
|
@@ -713,7 +748,9 @@ var init_Pipeline = __esm({
|
|
|
713
748
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
714
749
|
);
|
|
715
750
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
716
|
-
|
|
751
|
+
const generatedCount = cards.length;
|
|
752
|
+
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
753
|
+
cards = await this.hydrateTags(cards);
|
|
717
754
|
for (const filter of this.filters) {
|
|
718
755
|
const beforeCount = cards.length;
|
|
719
756
|
cards = await filter.transform(cards, context);
|
|
@@ -722,11 +759,33 @@ var init_Pipeline = __esm({
|
|
|
722
759
|
cards = cards.filter((c) => c.score > 0);
|
|
723
760
|
cards.sort((a, b) => b.score - a.score);
|
|
724
761
|
const result = cards.slice(0, limit);
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
);
|
|
762
|
+
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
763
|
+
logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
|
|
764
|
+
logCardProvenance(result, 3);
|
|
728
765
|
return result;
|
|
729
766
|
}
|
|
767
|
+
/**
|
|
768
|
+
* Batch hydrate tags for all cards.
|
|
769
|
+
*
|
|
770
|
+
* Fetches tags for all cards in a single database query and attaches them
|
|
771
|
+
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
772
|
+
* making individual getAppliedTags() calls.
|
|
773
|
+
*
|
|
774
|
+
* @param cards - Cards to hydrate
|
|
775
|
+
* @returns Cards with tags populated
|
|
776
|
+
*/
|
|
777
|
+
async hydrateTags(cards) {
|
|
778
|
+
if (cards.length === 0) {
|
|
779
|
+
return cards;
|
|
780
|
+
}
|
|
781
|
+
const cardIds = cards.map((c) => c.cardId);
|
|
782
|
+
const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
|
|
783
|
+
logTagHydration(cards, tagsByCard);
|
|
784
|
+
return cards.map((card) => ({
|
|
785
|
+
...card,
|
|
786
|
+
tags: tagsByCard.get(card.cardId) ?? []
|
|
787
|
+
}));
|
|
788
|
+
}
|
|
730
789
|
/**
|
|
731
790
|
* Build shared context for generator and filters.
|
|
732
791
|
*
|
|
@@ -1086,15 +1145,144 @@ var init_eloDistance = __esm({
|
|
|
1086
1145
|
}
|
|
1087
1146
|
});
|
|
1088
1147
|
|
|
1148
|
+
// src/core/navigators/filters/userTagPreference.ts
|
|
1149
|
+
var userTagPreference_exports = {};
|
|
1150
|
+
__export(userTagPreference_exports, {
|
|
1151
|
+
default: () => UserTagPreferenceFilter
|
|
1152
|
+
});
|
|
1153
|
+
var UserTagPreferenceFilter;
|
|
1154
|
+
var init_userTagPreference = __esm({
|
|
1155
|
+
"src/core/navigators/filters/userTagPreference.ts"() {
|
|
1156
|
+
"use strict";
|
|
1157
|
+
init_navigators();
|
|
1158
|
+
UserTagPreferenceFilter = class extends ContentNavigator {
|
|
1159
|
+
_strategyData;
|
|
1160
|
+
/** Human-readable name for CardFilter interface */
|
|
1161
|
+
name;
|
|
1162
|
+
constructor(user, course, strategyData) {
|
|
1163
|
+
super(user, course, strategyData);
|
|
1164
|
+
this._strategyData = strategyData;
|
|
1165
|
+
this.name = strategyData.name || "User Tag Preferences";
|
|
1166
|
+
}
|
|
1167
|
+
/**
|
|
1168
|
+
* Compute multiplier for a card based on its tags and user preferences.
|
|
1169
|
+
* Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
|
|
1170
|
+
*/
|
|
1171
|
+
computeMultiplier(cardTags, boostMap) {
|
|
1172
|
+
const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
|
|
1173
|
+
if (multipliers.length === 0) {
|
|
1174
|
+
return 1;
|
|
1175
|
+
}
|
|
1176
|
+
return Math.max(...multipliers);
|
|
1177
|
+
}
|
|
1178
|
+
/**
|
|
1179
|
+
* Build human-readable reason for the filter's decision.
|
|
1180
|
+
*/
|
|
1181
|
+
buildReason(cardTags, boostMap, multiplier) {
|
|
1182
|
+
const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
|
|
1183
|
+
if (multiplier === 0) {
|
|
1184
|
+
return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
|
|
1185
|
+
}
|
|
1186
|
+
if (multiplier < 1) {
|
|
1187
|
+
return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1188
|
+
}
|
|
1189
|
+
if (multiplier > 1) {
|
|
1190
|
+
return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1191
|
+
}
|
|
1192
|
+
return "No matching user preferences";
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* CardFilter.transform implementation.
|
|
1196
|
+
*
|
|
1197
|
+
* Apply user tag preferences:
|
|
1198
|
+
* 1. Read preferences from strategy state
|
|
1199
|
+
* 2. If no preferences, pass through unchanged
|
|
1200
|
+
* 3. For each card:
|
|
1201
|
+
* - Look up tag in boost record
|
|
1202
|
+
* - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
|
|
1203
|
+
* - If multiple tags match: use max multiplier
|
|
1204
|
+
* - Append provenance with clear reason
|
|
1205
|
+
*/
|
|
1206
|
+
async transform(cards, _context) {
|
|
1207
|
+
const prefs = await this.getStrategyState();
|
|
1208
|
+
if (!prefs || Object.keys(prefs.boost).length === 0) {
|
|
1209
|
+
return cards.map((card) => ({
|
|
1210
|
+
...card,
|
|
1211
|
+
provenance: [
|
|
1212
|
+
...card.provenance,
|
|
1213
|
+
{
|
|
1214
|
+
strategy: "userTagPreference",
|
|
1215
|
+
strategyName: this.strategyName || this.name,
|
|
1216
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1217
|
+
action: "passed",
|
|
1218
|
+
score: card.score,
|
|
1219
|
+
reason: "No user tag preferences configured"
|
|
1220
|
+
}
|
|
1221
|
+
]
|
|
1222
|
+
}));
|
|
1223
|
+
}
|
|
1224
|
+
const adjusted = await Promise.all(
|
|
1225
|
+
cards.map(async (card) => {
|
|
1226
|
+
const cardTags = card.tags ?? [];
|
|
1227
|
+
const multiplier = this.computeMultiplier(cardTags, prefs.boost);
|
|
1228
|
+
const finalScore = Math.min(1, card.score * multiplier);
|
|
1229
|
+
let action;
|
|
1230
|
+
if (multiplier === 0 || multiplier < 1) {
|
|
1231
|
+
action = "penalized";
|
|
1232
|
+
} else if (multiplier > 1) {
|
|
1233
|
+
action = "boosted";
|
|
1234
|
+
} else {
|
|
1235
|
+
action = "passed";
|
|
1236
|
+
}
|
|
1237
|
+
return {
|
|
1238
|
+
...card,
|
|
1239
|
+
score: finalScore,
|
|
1240
|
+
provenance: [
|
|
1241
|
+
...card.provenance,
|
|
1242
|
+
{
|
|
1243
|
+
strategy: "userTagPreference",
|
|
1244
|
+
strategyName: this.strategyName || this.name,
|
|
1245
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1246
|
+
action,
|
|
1247
|
+
score: finalScore,
|
|
1248
|
+
reason: this.buildReason(cardTags, prefs.boost, multiplier)
|
|
1249
|
+
}
|
|
1250
|
+
]
|
|
1251
|
+
};
|
|
1252
|
+
})
|
|
1253
|
+
);
|
|
1254
|
+
return adjusted;
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Legacy getWeightedCards - throws as filters should not be used as generators.
|
|
1258
|
+
*/
|
|
1259
|
+
async getWeightedCards(_limit) {
|
|
1260
|
+
throw new Error(
|
|
1261
|
+
"UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
// Legacy methods - stub implementations since filters don't generate cards
|
|
1265
|
+
async getNewCards(_n) {
|
|
1266
|
+
return [];
|
|
1267
|
+
}
|
|
1268
|
+
async getPendingReviews() {
|
|
1269
|
+
return [];
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
});
|
|
1274
|
+
|
|
1089
1275
|
// src/core/navigators/filters/index.ts
|
|
1090
1276
|
var filters_exports = {};
|
|
1091
1277
|
__export(filters_exports, {
|
|
1278
|
+
UserTagPreferenceFilter: () => UserTagPreferenceFilter,
|
|
1092
1279
|
createEloDistanceFilter: () => createEloDistanceFilter
|
|
1093
1280
|
});
|
|
1094
1281
|
var init_filters = __esm({
|
|
1095
1282
|
"src/core/navigators/filters/index.ts"() {
|
|
1096
1283
|
"use strict";
|
|
1097
1284
|
init_eloDistance();
|
|
1285
|
+
init_userTagPreference();
|
|
1098
1286
|
}
|
|
1099
1287
|
});
|
|
1100
1288
|
|
|
@@ -1327,10 +1515,9 @@ var init_hierarchyDefinition = __esm({
|
|
|
1327
1515
|
/**
|
|
1328
1516
|
* Check if a card is unlocked and generate reason.
|
|
1329
1517
|
*/
|
|
1330
|
-
async checkCardUnlock(
|
|
1518
|
+
async checkCardUnlock(card, course, unlockedTags, masteredTags) {
|
|
1331
1519
|
try {
|
|
1332
|
-
const
|
|
1333
|
-
const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
|
|
1520
|
+
const cardTags = card.tags ?? [];
|
|
1334
1521
|
const lockedTags = cardTags.filter(
|
|
1335
1522
|
(tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
|
|
1336
1523
|
);
|
|
@@ -1367,7 +1554,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1367
1554
|
const gated = [];
|
|
1368
1555
|
for (const card of cards) {
|
|
1369
1556
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
1370
|
-
card
|
|
1557
|
+
card,
|
|
1371
1558
|
context.course,
|
|
1372
1559
|
unlockedTags,
|
|
1373
1560
|
masteredTags
|
|
@@ -1413,6 +1600,19 @@ var init_hierarchyDefinition = __esm({
|
|
|
1413
1600
|
}
|
|
1414
1601
|
});
|
|
1415
1602
|
|
|
1603
|
+
// src/core/navigators/inferredPreference.ts
|
|
1604
|
+
var inferredPreference_exports = {};
|
|
1605
|
+
__export(inferredPreference_exports, {
|
|
1606
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
|
|
1607
|
+
});
|
|
1608
|
+
var INFERRED_PREFERENCE_NAVIGATOR_STUB;
|
|
1609
|
+
var init_inferredPreference = __esm({
|
|
1610
|
+
"src/core/navigators/inferredPreference.ts"() {
|
|
1611
|
+
"use strict";
|
|
1612
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1416
1616
|
// src/core/navigators/interferenceMitigator.ts
|
|
1417
1617
|
var interferenceMitigator_exports = {};
|
|
1418
1618
|
__export(interferenceMitigator_exports, {
|
|
@@ -1544,17 +1744,6 @@ var init_interferenceMitigator = __esm({
|
|
|
1544
1744
|
}
|
|
1545
1745
|
return avoid;
|
|
1546
1746
|
}
|
|
1547
|
-
/**
|
|
1548
|
-
* Get tags for a single card
|
|
1549
|
-
*/
|
|
1550
|
-
async getCardTags(cardId, course) {
|
|
1551
|
-
try {
|
|
1552
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
1553
|
-
return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
|
|
1554
|
-
} catch {
|
|
1555
|
-
return [];
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
1747
|
/**
|
|
1559
1748
|
* Compute interference score reduction for a card.
|
|
1560
1749
|
* Returns: { multiplier, interfering tags, reason }
|
|
@@ -1606,7 +1795,7 @@ var init_interferenceMitigator = __esm({
|
|
|
1606
1795
|
const tagsToAvoid = this.getTagsToAvoid(immatureTags);
|
|
1607
1796
|
const adjusted = [];
|
|
1608
1797
|
for (const card of cards) {
|
|
1609
|
-
const cardTags =
|
|
1798
|
+
const cardTags = card.tags ?? [];
|
|
1610
1799
|
const { multiplier, reason } = this.computeInterferenceEffect(
|
|
1611
1800
|
cardTags,
|
|
1612
1801
|
tagsToAvoid,
|
|
@@ -1751,27 +1940,16 @@ var init_relativePriority = __esm({
|
|
|
1751
1940
|
return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
|
|
1752
1941
|
}
|
|
1753
1942
|
}
|
|
1754
|
-
/**
|
|
1755
|
-
* Get tags for a single card.
|
|
1756
|
-
*/
|
|
1757
|
-
async getCardTags(cardId, course) {
|
|
1758
|
-
try {
|
|
1759
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
1760
|
-
return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
|
|
1761
|
-
} catch {
|
|
1762
|
-
return [];
|
|
1763
|
-
}
|
|
1764
|
-
}
|
|
1765
1943
|
/**
|
|
1766
1944
|
* CardFilter.transform implementation.
|
|
1767
1945
|
*
|
|
1768
1946
|
* Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
|
|
1769
1947
|
* cards with low-priority tags get reduced scores.
|
|
1770
1948
|
*/
|
|
1771
|
-
async transform(cards,
|
|
1949
|
+
async transform(cards, _context) {
|
|
1772
1950
|
const adjusted = await Promise.all(
|
|
1773
1951
|
cards.map(async (card) => {
|
|
1774
|
-
const cardTags =
|
|
1952
|
+
const cardTags = card.tags ?? [];
|
|
1775
1953
|
const priority = this.computeCardPriority(cardTags);
|
|
1776
1954
|
const boostFactor = this.computeBoostFactor(priority);
|
|
1777
1955
|
const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
|
|
@@ -1938,6 +2116,19 @@ var init_srs = __esm({
|
|
|
1938
2116
|
}
|
|
1939
2117
|
});
|
|
1940
2118
|
|
|
2119
|
+
// src/core/navigators/userGoal.ts
|
|
2120
|
+
var userGoal_exports = {};
|
|
2121
|
+
__export(userGoal_exports, {
|
|
2122
|
+
USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
|
|
2123
|
+
});
|
|
2124
|
+
var USER_GOAL_NAVIGATOR_STUB;
|
|
2125
|
+
var init_userGoal = __esm({
|
|
2126
|
+
"src/core/navigators/userGoal.ts"() {
|
|
2127
|
+
"use strict";
|
|
2128
|
+
USER_GOAL_NAVIGATOR_STUB = true;
|
|
2129
|
+
}
|
|
2130
|
+
});
|
|
2131
|
+
|
|
1941
2132
|
// import("./**/*") in src/core/navigators/index.ts
|
|
1942
2133
|
var globImport;
|
|
1943
2134
|
var init_ = __esm({
|
|
@@ -1950,14 +2141,17 @@ var init_ = __esm({
|
|
|
1950
2141
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
1951
2142
|
"./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
|
|
1952
2143
|
"./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
2144
|
+
"./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
|
|
1953
2145
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
1954
2146
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
|
|
1955
2147
|
"./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
|
|
1956
2148
|
"./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
1957
2149
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
|
|
2150
|
+
"./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
|
|
1958
2151
|
"./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
|
|
1959
2152
|
"./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
|
|
1960
|
-
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
2153
|
+
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
2154
|
+
"./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
|
|
1961
2155
|
});
|
|
1962
2156
|
}
|
|
1963
2157
|
});
|
|
@@ -2006,6 +2200,7 @@ var init_navigators = __esm({
|
|
|
2006
2200
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
2007
2201
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
2008
2202
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
2203
|
+
Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
|
|
2009
2204
|
return Navigators2;
|
|
2010
2205
|
})(Navigators || {});
|
|
2011
2206
|
NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
|
|
@@ -2019,7 +2214,8 @@ var init_navigators = __esm({
|
|
|
2019
2214
|
["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
|
|
2020
2215
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
2021
2216
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
2022
|
-
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER
|
|
2217
|
+
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
2218
|
+
["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
|
|
2023
2219
|
};
|
|
2024
2220
|
ContentNavigator = class {
|
|
2025
2221
|
/** User interface for this navigation session */
|
|
@@ -2044,6 +2240,52 @@ var init_navigators = __esm({
|
|
|
2044
2240
|
this.strategyId = strategyData._id;
|
|
2045
2241
|
}
|
|
2046
2242
|
}
|
|
2243
|
+
// ============================================================================
|
|
2244
|
+
// STRATEGY STATE HELPERS
|
|
2245
|
+
// ============================================================================
|
|
2246
|
+
//
|
|
2247
|
+
// These methods allow strategies to persist their own state (user preferences,
|
|
2248
|
+
// learned patterns, temporal tracking) in the user database.
|
|
2249
|
+
//
|
|
2250
|
+
// ============================================================================
|
|
2251
|
+
/**
|
|
2252
|
+
* Unique key identifying this strategy for state storage.
|
|
2253
|
+
*
|
|
2254
|
+
* Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
|
|
2255
|
+
* Override in subclasses if multiple instances of the same strategy type
|
|
2256
|
+
* need separate state storage.
|
|
2257
|
+
*/
|
|
2258
|
+
get strategyKey() {
|
|
2259
|
+
return this.constructor.name;
|
|
2260
|
+
}
|
|
2261
|
+
/**
|
|
2262
|
+
* Get this strategy's persisted state for the current course.
|
|
2263
|
+
*
|
|
2264
|
+
* @returns The strategy's data payload, or null if no state exists
|
|
2265
|
+
* @throws Error if user or course is not initialized
|
|
2266
|
+
*/
|
|
2267
|
+
async getStrategyState() {
|
|
2268
|
+
if (!this.user || !this.course) {
|
|
2269
|
+
throw new Error(
|
|
2270
|
+
`Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
|
|
2274
|
+
}
|
|
2275
|
+
/**
|
|
2276
|
+
* Persist this strategy's state for the current course.
|
|
2277
|
+
*
|
|
2278
|
+
* @param data - The strategy's data payload to store
|
|
2279
|
+
* @throws Error if user or course is not initialized
|
|
2280
|
+
*/
|
|
2281
|
+
async putStrategyState(data) {
|
|
2282
|
+
if (!this.user || !this.course) {
|
|
2283
|
+
throw new Error(
|
|
2284
|
+
`Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2285
|
+
);
|
|
2286
|
+
}
|
|
2287
|
+
return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
|
|
2288
|
+
}
|
|
2047
2289
|
/**
|
|
2048
2290
|
* Factory method to create navigator instances dynamically.
|
|
2049
2291
|
*
|
|
@@ -2274,7 +2516,9 @@ import moment6 from "moment";
|
|
|
2274
2516
|
function accomodateGuest() {
|
|
2275
2517
|
logger.log("[funnel] accomodateGuest() called");
|
|
2276
2518
|
if (typeof localStorage === "undefined") {
|
|
2277
|
-
logger.log(
|
|
2519
|
+
logger.log(
|
|
2520
|
+
"[funnel] localStorage not available (Node.js environment), returning default guest"
|
|
2521
|
+
);
|
|
2278
2522
|
return {
|
|
2279
2523
|
username: GuestUsername + "nodejs-test",
|
|
2280
2524
|
firstVisit: true
|
|
@@ -3252,6 +3496,55 @@ Currently logged-in as ${this._username}.`
|
|
|
3252
3496
|
async updateUserElo(courseId, elo) {
|
|
3253
3497
|
return updateUserElo(this._username, courseId, elo);
|
|
3254
3498
|
}
|
|
3499
|
+
async getStrategyState(courseId, strategyKey) {
|
|
3500
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
3501
|
+
try {
|
|
3502
|
+
const doc = await this.localDB.get(docId);
|
|
3503
|
+
return doc.data;
|
|
3504
|
+
} catch (e) {
|
|
3505
|
+
const err = e;
|
|
3506
|
+
if (err.status === 404) {
|
|
3507
|
+
return null;
|
|
3508
|
+
}
|
|
3509
|
+
throw e;
|
|
3510
|
+
}
|
|
3511
|
+
}
|
|
3512
|
+
async putStrategyState(courseId, strategyKey, data) {
|
|
3513
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
3514
|
+
let existingRev;
|
|
3515
|
+
try {
|
|
3516
|
+
const existing = await this.localDB.get(docId);
|
|
3517
|
+
existingRev = existing._rev;
|
|
3518
|
+
} catch (e) {
|
|
3519
|
+
const err = e;
|
|
3520
|
+
if (err.status !== 404) {
|
|
3521
|
+
throw e;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
const doc = {
|
|
3525
|
+
_id: docId,
|
|
3526
|
+
_rev: existingRev,
|
|
3527
|
+
docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
|
|
3528
|
+
courseId,
|
|
3529
|
+
strategyKey,
|
|
3530
|
+
data,
|
|
3531
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3532
|
+
};
|
|
3533
|
+
await this.localDB.put(doc);
|
|
3534
|
+
}
|
|
3535
|
+
async deleteStrategyState(courseId, strategyKey) {
|
|
3536
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
3537
|
+
try {
|
|
3538
|
+
const doc = await this.localDB.get(docId);
|
|
3539
|
+
await this.localDB.remove(doc);
|
|
3540
|
+
} catch (e) {
|
|
3541
|
+
const err = e;
|
|
3542
|
+
if (err.status === 404) {
|
|
3543
|
+
return;
|
|
3544
|
+
}
|
|
3545
|
+
throw e;
|
|
3546
|
+
}
|
|
3547
|
+
}
|
|
3255
3548
|
};
|
|
3256
3549
|
userCoursesDoc = "CourseRegistrations";
|
|
3257
3550
|
userClassroomsDoc = "ClassroomRegistrations";
|
|
@@ -3346,6 +3639,16 @@ var init_user = __esm({
|
|
|
3346
3639
|
}
|
|
3347
3640
|
});
|
|
3348
3641
|
|
|
3642
|
+
// src/core/types/strategyState.ts
|
|
3643
|
+
function buildStrategyStateId(courseId, strategyKey) {
|
|
3644
|
+
return `STRATEGY_STATE::${courseId}::${strategyKey}`;
|
|
3645
|
+
}
|
|
3646
|
+
var init_strategyState = __esm({
|
|
3647
|
+
"src/core/types/strategyState.ts"() {
|
|
3648
|
+
"use strict";
|
|
3649
|
+
}
|
|
3650
|
+
});
|
|
3651
|
+
|
|
3349
3652
|
// src/core/bulkImport/cardProcessor.ts
|
|
3350
3653
|
import { Status as Status4 } from "@vue-skuilder/common";
|
|
3351
3654
|
var init_cardProcessor = __esm({
|
|
@@ -3378,6 +3681,7 @@ var init_core = __esm({
|
|
|
3378
3681
|
init_interfaces();
|
|
3379
3682
|
init_types_legacy();
|
|
3380
3683
|
init_user();
|
|
3684
|
+
init_strategyState();
|
|
3381
3685
|
init_Loggable();
|
|
3382
3686
|
init_util();
|
|
3383
3687
|
init_navigators();
|
|
@@ -3950,6 +4254,14 @@ var init_courseDB3 = __esm({
|
|
|
3950
4254
|
};
|
|
3951
4255
|
}
|
|
3952
4256
|
}
|
|
4257
|
+
async getAppliedTagsBatch(cardIds) {
|
|
4258
|
+
const tagsIndex = await this.unpacker.getTagsIndex();
|
|
4259
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
4260
|
+
for (const cardId of cardIds) {
|
|
4261
|
+
tagsByCard.set(cardId, tagsIndex.byCard[cardId] || []);
|
|
4262
|
+
}
|
|
4263
|
+
return tagsByCard;
|
|
4264
|
+
}
|
|
3953
4265
|
async addTagToCard(_cardId, _tagId) {
|
|
3954
4266
|
throw new Error("Cannot modify tags in static mode");
|
|
3955
4267
|
}
|