@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
|
@@ -258,7 +258,8 @@ var init_types_legacy = __esm({
|
|
|
258
258
|
["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
|
|
259
259
|
["VIEW" /* VIEW */]: "VIEW",
|
|
260
260
|
["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
|
|
261
|
-
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
|
|
261
|
+
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY",
|
|
262
|
+
["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE"
|
|
262
263
|
};
|
|
263
264
|
}
|
|
264
265
|
});
|
|
@@ -789,6 +790,41 @@ __export(Pipeline_exports, {
|
|
|
789
790
|
Pipeline: () => Pipeline
|
|
790
791
|
});
|
|
791
792
|
import { toCourseElo as toCourseElo2 } from "@vue-skuilder/common";
|
|
793
|
+
function logPipelineConfig(generator, filters) {
|
|
794
|
+
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
795
|
+
logger.info(
|
|
796
|
+
`[Pipeline] Configuration:
|
|
797
|
+
Generator: ${generator.name}
|
|
798
|
+
Filters:${filterList}`
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
function logTagHydration(cards, tagsByCard) {
|
|
802
|
+
const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
|
|
803
|
+
const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
|
|
804
|
+
logger.debug(
|
|
805
|
+
`[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
|
|
809
|
+
const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
|
|
810
|
+
logger.info(
|
|
811
|
+
`[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
function logCardProvenance(cards, maxCards = 3) {
|
|
815
|
+
const cardsToLog = cards.slice(0, maxCards);
|
|
816
|
+
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
817
|
+
for (const card of cardsToLog) {
|
|
818
|
+
logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
|
|
819
|
+
for (const entry of card.provenance) {
|
|
820
|
+
const scoreChange = entry.score.toFixed(3);
|
|
821
|
+
const action = entry.action.padEnd(9);
|
|
822
|
+
logger.debug(
|
|
823
|
+
`[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
792
828
|
var Pipeline;
|
|
793
829
|
var init_Pipeline = __esm({
|
|
794
830
|
"src/core/navigators/Pipeline.ts"() {
|
|
@@ -812,19 +848,18 @@ var init_Pipeline = __esm({
|
|
|
812
848
|
this.filters = filters;
|
|
813
849
|
this.user = user;
|
|
814
850
|
this.course = course;
|
|
815
|
-
|
|
816
|
-
`[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
|
|
817
|
-
);
|
|
851
|
+
logPipelineConfig(generator, filters);
|
|
818
852
|
}
|
|
819
853
|
/**
|
|
820
854
|
* Get weighted cards by running generator and applying filters.
|
|
821
855
|
*
|
|
822
856
|
* 1. Build shared context (user ELO, etc.)
|
|
823
857
|
* 2. Get candidates from generator (passing context)
|
|
824
|
-
* 3.
|
|
825
|
-
* 4.
|
|
826
|
-
* 5.
|
|
827
|
-
* 6.
|
|
858
|
+
* 3. Batch hydrate tags for all candidates
|
|
859
|
+
* 4. Apply each filter sequentially
|
|
860
|
+
* 5. Remove zero-score cards
|
|
861
|
+
* 6. Sort by score descending
|
|
862
|
+
* 7. Return top N
|
|
828
863
|
*
|
|
829
864
|
* @param limit - Maximum number of cards to return
|
|
830
865
|
* @returns Cards sorted by score descending
|
|
@@ -837,7 +872,9 @@ var init_Pipeline = __esm({
|
|
|
837
872
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
838
873
|
);
|
|
839
874
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
840
|
-
|
|
875
|
+
const generatedCount = cards.length;
|
|
876
|
+
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
877
|
+
cards = await this.hydrateTags(cards);
|
|
841
878
|
for (const filter of this.filters) {
|
|
842
879
|
const beforeCount = cards.length;
|
|
843
880
|
cards = await filter.transform(cards, context);
|
|
@@ -846,11 +883,33 @@ var init_Pipeline = __esm({
|
|
|
846
883
|
cards = cards.filter((c) => c.score > 0);
|
|
847
884
|
cards.sort((a, b) => b.score - a.score);
|
|
848
885
|
const result = cards.slice(0, limit);
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
);
|
|
886
|
+
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
887
|
+
logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
|
|
888
|
+
logCardProvenance(result, 3);
|
|
852
889
|
return result;
|
|
853
890
|
}
|
|
891
|
+
/**
|
|
892
|
+
* Batch hydrate tags for all cards.
|
|
893
|
+
*
|
|
894
|
+
* Fetches tags for all cards in a single database query and attaches them
|
|
895
|
+
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
896
|
+
* making individual getAppliedTags() calls.
|
|
897
|
+
*
|
|
898
|
+
* @param cards - Cards to hydrate
|
|
899
|
+
* @returns Cards with tags populated
|
|
900
|
+
*/
|
|
901
|
+
async hydrateTags(cards) {
|
|
902
|
+
if (cards.length === 0) {
|
|
903
|
+
return cards;
|
|
904
|
+
}
|
|
905
|
+
const cardIds = cards.map((c) => c.cardId);
|
|
906
|
+
const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
|
|
907
|
+
logTagHydration(cards, tagsByCard);
|
|
908
|
+
return cards.map((card) => ({
|
|
909
|
+
...card,
|
|
910
|
+
tags: tagsByCard.get(card.cardId) ?? []
|
|
911
|
+
}));
|
|
912
|
+
}
|
|
854
913
|
/**
|
|
855
914
|
* Build shared context for generator and filters.
|
|
856
915
|
*
|
|
@@ -1210,15 +1269,144 @@ var init_eloDistance = __esm({
|
|
|
1210
1269
|
}
|
|
1211
1270
|
});
|
|
1212
1271
|
|
|
1272
|
+
// src/core/navigators/filters/userTagPreference.ts
|
|
1273
|
+
var userTagPreference_exports = {};
|
|
1274
|
+
__export(userTagPreference_exports, {
|
|
1275
|
+
default: () => UserTagPreferenceFilter
|
|
1276
|
+
});
|
|
1277
|
+
var UserTagPreferenceFilter;
|
|
1278
|
+
var init_userTagPreference = __esm({
|
|
1279
|
+
"src/core/navigators/filters/userTagPreference.ts"() {
|
|
1280
|
+
"use strict";
|
|
1281
|
+
init_navigators();
|
|
1282
|
+
UserTagPreferenceFilter = class extends ContentNavigator {
|
|
1283
|
+
_strategyData;
|
|
1284
|
+
/** Human-readable name for CardFilter interface */
|
|
1285
|
+
name;
|
|
1286
|
+
constructor(user, course, strategyData) {
|
|
1287
|
+
super(user, course, strategyData);
|
|
1288
|
+
this._strategyData = strategyData;
|
|
1289
|
+
this.name = strategyData.name || "User Tag Preferences";
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Compute multiplier for a card based on its tags and user preferences.
|
|
1293
|
+
* Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
|
|
1294
|
+
*/
|
|
1295
|
+
computeMultiplier(cardTags, boostMap) {
|
|
1296
|
+
const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
|
|
1297
|
+
if (multipliers.length === 0) {
|
|
1298
|
+
return 1;
|
|
1299
|
+
}
|
|
1300
|
+
return Math.max(...multipliers);
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* Build human-readable reason for the filter's decision.
|
|
1304
|
+
*/
|
|
1305
|
+
buildReason(cardTags, boostMap, multiplier) {
|
|
1306
|
+
const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
|
|
1307
|
+
if (multiplier === 0) {
|
|
1308
|
+
return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
|
|
1309
|
+
}
|
|
1310
|
+
if (multiplier < 1) {
|
|
1311
|
+
return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1312
|
+
}
|
|
1313
|
+
if (multiplier > 1) {
|
|
1314
|
+
return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1315
|
+
}
|
|
1316
|
+
return "No matching user preferences";
|
|
1317
|
+
}
|
|
1318
|
+
/**
|
|
1319
|
+
* CardFilter.transform implementation.
|
|
1320
|
+
*
|
|
1321
|
+
* Apply user tag preferences:
|
|
1322
|
+
* 1. Read preferences from strategy state
|
|
1323
|
+
* 2. If no preferences, pass through unchanged
|
|
1324
|
+
* 3. For each card:
|
|
1325
|
+
* - Look up tag in boost record
|
|
1326
|
+
* - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
|
|
1327
|
+
* - If multiple tags match: use max multiplier
|
|
1328
|
+
* - Append provenance with clear reason
|
|
1329
|
+
*/
|
|
1330
|
+
async transform(cards, _context) {
|
|
1331
|
+
const prefs = await this.getStrategyState();
|
|
1332
|
+
if (!prefs || Object.keys(prefs.boost).length === 0) {
|
|
1333
|
+
return cards.map((card) => ({
|
|
1334
|
+
...card,
|
|
1335
|
+
provenance: [
|
|
1336
|
+
...card.provenance,
|
|
1337
|
+
{
|
|
1338
|
+
strategy: "userTagPreference",
|
|
1339
|
+
strategyName: this.strategyName || this.name,
|
|
1340
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1341
|
+
action: "passed",
|
|
1342
|
+
score: card.score,
|
|
1343
|
+
reason: "No user tag preferences configured"
|
|
1344
|
+
}
|
|
1345
|
+
]
|
|
1346
|
+
}));
|
|
1347
|
+
}
|
|
1348
|
+
const adjusted = await Promise.all(
|
|
1349
|
+
cards.map(async (card) => {
|
|
1350
|
+
const cardTags = card.tags ?? [];
|
|
1351
|
+
const multiplier = this.computeMultiplier(cardTags, prefs.boost);
|
|
1352
|
+
const finalScore = Math.min(1, card.score * multiplier);
|
|
1353
|
+
let action;
|
|
1354
|
+
if (multiplier === 0 || multiplier < 1) {
|
|
1355
|
+
action = "penalized";
|
|
1356
|
+
} else if (multiplier > 1) {
|
|
1357
|
+
action = "boosted";
|
|
1358
|
+
} else {
|
|
1359
|
+
action = "passed";
|
|
1360
|
+
}
|
|
1361
|
+
return {
|
|
1362
|
+
...card,
|
|
1363
|
+
score: finalScore,
|
|
1364
|
+
provenance: [
|
|
1365
|
+
...card.provenance,
|
|
1366
|
+
{
|
|
1367
|
+
strategy: "userTagPreference",
|
|
1368
|
+
strategyName: this.strategyName || this.name,
|
|
1369
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1370
|
+
action,
|
|
1371
|
+
score: finalScore,
|
|
1372
|
+
reason: this.buildReason(cardTags, prefs.boost, multiplier)
|
|
1373
|
+
}
|
|
1374
|
+
]
|
|
1375
|
+
};
|
|
1376
|
+
})
|
|
1377
|
+
);
|
|
1378
|
+
return adjusted;
|
|
1379
|
+
}
|
|
1380
|
+
/**
|
|
1381
|
+
* Legacy getWeightedCards - throws as filters should not be used as generators.
|
|
1382
|
+
*/
|
|
1383
|
+
async getWeightedCards(_limit) {
|
|
1384
|
+
throw new Error(
|
|
1385
|
+
"UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
|
|
1386
|
+
);
|
|
1387
|
+
}
|
|
1388
|
+
// Legacy methods - stub implementations since filters don't generate cards
|
|
1389
|
+
async getNewCards(_n) {
|
|
1390
|
+
return [];
|
|
1391
|
+
}
|
|
1392
|
+
async getPendingReviews() {
|
|
1393
|
+
return [];
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1213
1399
|
// src/core/navigators/filters/index.ts
|
|
1214
1400
|
var filters_exports = {};
|
|
1215
1401
|
__export(filters_exports, {
|
|
1402
|
+
UserTagPreferenceFilter: () => UserTagPreferenceFilter,
|
|
1216
1403
|
createEloDistanceFilter: () => createEloDistanceFilter
|
|
1217
1404
|
});
|
|
1218
1405
|
var init_filters = __esm({
|
|
1219
1406
|
"src/core/navigators/filters/index.ts"() {
|
|
1220
1407
|
"use strict";
|
|
1221
1408
|
init_eloDistance();
|
|
1409
|
+
init_userTagPreference();
|
|
1222
1410
|
}
|
|
1223
1411
|
});
|
|
1224
1412
|
|
|
@@ -1451,10 +1639,9 @@ var init_hierarchyDefinition = __esm({
|
|
|
1451
1639
|
/**
|
|
1452
1640
|
* Check if a card is unlocked and generate reason.
|
|
1453
1641
|
*/
|
|
1454
|
-
async checkCardUnlock(
|
|
1642
|
+
async checkCardUnlock(card, course, unlockedTags, masteredTags) {
|
|
1455
1643
|
try {
|
|
1456
|
-
const
|
|
1457
|
-
const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
|
|
1644
|
+
const cardTags = card.tags ?? [];
|
|
1458
1645
|
const lockedTags = cardTags.filter(
|
|
1459
1646
|
(tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
|
|
1460
1647
|
);
|
|
@@ -1491,7 +1678,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1491
1678
|
const gated = [];
|
|
1492
1679
|
for (const card of cards) {
|
|
1493
1680
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
1494
|
-
card
|
|
1681
|
+
card,
|
|
1495
1682
|
context.course,
|
|
1496
1683
|
unlockedTags,
|
|
1497
1684
|
masteredTags
|
|
@@ -1537,6 +1724,19 @@ var init_hierarchyDefinition = __esm({
|
|
|
1537
1724
|
}
|
|
1538
1725
|
});
|
|
1539
1726
|
|
|
1727
|
+
// src/core/navigators/inferredPreference.ts
|
|
1728
|
+
var inferredPreference_exports = {};
|
|
1729
|
+
__export(inferredPreference_exports, {
|
|
1730
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
|
|
1731
|
+
});
|
|
1732
|
+
var INFERRED_PREFERENCE_NAVIGATOR_STUB;
|
|
1733
|
+
var init_inferredPreference = __esm({
|
|
1734
|
+
"src/core/navigators/inferredPreference.ts"() {
|
|
1735
|
+
"use strict";
|
|
1736
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
|
|
1540
1740
|
// src/core/navigators/interferenceMitigator.ts
|
|
1541
1741
|
var interferenceMitigator_exports = {};
|
|
1542
1742
|
__export(interferenceMitigator_exports, {
|
|
@@ -1668,17 +1868,6 @@ var init_interferenceMitigator = __esm({
|
|
|
1668
1868
|
}
|
|
1669
1869
|
return avoid;
|
|
1670
1870
|
}
|
|
1671
|
-
/**
|
|
1672
|
-
* Get tags for a single card
|
|
1673
|
-
*/
|
|
1674
|
-
async getCardTags(cardId, course) {
|
|
1675
|
-
try {
|
|
1676
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
1677
|
-
return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
|
|
1678
|
-
} catch {
|
|
1679
|
-
return [];
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
1871
|
/**
|
|
1683
1872
|
* Compute interference score reduction for a card.
|
|
1684
1873
|
* Returns: { multiplier, interfering tags, reason }
|
|
@@ -1730,7 +1919,7 @@ var init_interferenceMitigator = __esm({
|
|
|
1730
1919
|
const tagsToAvoid = this.getTagsToAvoid(immatureTags);
|
|
1731
1920
|
const adjusted = [];
|
|
1732
1921
|
for (const card of cards) {
|
|
1733
|
-
const cardTags =
|
|
1922
|
+
const cardTags = card.tags ?? [];
|
|
1734
1923
|
const { multiplier, reason } = this.computeInterferenceEffect(
|
|
1735
1924
|
cardTags,
|
|
1736
1925
|
tagsToAvoid,
|
|
@@ -1875,27 +2064,16 @@ var init_relativePriority = __esm({
|
|
|
1875
2064
|
return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
|
|
1876
2065
|
}
|
|
1877
2066
|
}
|
|
1878
|
-
/**
|
|
1879
|
-
* Get tags for a single card.
|
|
1880
|
-
*/
|
|
1881
|
-
async getCardTags(cardId, course) {
|
|
1882
|
-
try {
|
|
1883
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
1884
|
-
return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
|
|
1885
|
-
} catch {
|
|
1886
|
-
return [];
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
2067
|
/**
|
|
1890
2068
|
* CardFilter.transform implementation.
|
|
1891
2069
|
*
|
|
1892
2070
|
* Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
|
|
1893
2071
|
* cards with low-priority tags get reduced scores.
|
|
1894
2072
|
*/
|
|
1895
|
-
async transform(cards,
|
|
2073
|
+
async transform(cards, _context) {
|
|
1896
2074
|
const adjusted = await Promise.all(
|
|
1897
2075
|
cards.map(async (card) => {
|
|
1898
|
-
const cardTags =
|
|
2076
|
+
const cardTags = card.tags ?? [];
|
|
1899
2077
|
const priority = this.computeCardPriority(cardTags);
|
|
1900
2078
|
const boostFactor = this.computeBoostFactor(priority);
|
|
1901
2079
|
const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
|
|
@@ -2062,6 +2240,19 @@ var init_srs = __esm({
|
|
|
2062
2240
|
}
|
|
2063
2241
|
});
|
|
2064
2242
|
|
|
2243
|
+
// src/core/navigators/userGoal.ts
|
|
2244
|
+
var userGoal_exports = {};
|
|
2245
|
+
__export(userGoal_exports, {
|
|
2246
|
+
USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
|
|
2247
|
+
});
|
|
2248
|
+
var USER_GOAL_NAVIGATOR_STUB;
|
|
2249
|
+
var init_userGoal = __esm({
|
|
2250
|
+
"src/core/navigators/userGoal.ts"() {
|
|
2251
|
+
"use strict";
|
|
2252
|
+
USER_GOAL_NAVIGATOR_STUB = true;
|
|
2253
|
+
}
|
|
2254
|
+
});
|
|
2255
|
+
|
|
2065
2256
|
// import("./**/*") in src/core/navigators/index.ts
|
|
2066
2257
|
var globImport;
|
|
2067
2258
|
var init_ = __esm({
|
|
@@ -2074,14 +2265,17 @@ var init_ = __esm({
|
|
|
2074
2265
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
2075
2266
|
"./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
|
|
2076
2267
|
"./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
2268
|
+
"./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
|
|
2077
2269
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
2078
2270
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
|
|
2079
2271
|
"./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
|
|
2080
2272
|
"./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
2081
2273
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
|
|
2274
|
+
"./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
|
|
2082
2275
|
"./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
|
|
2083
2276
|
"./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
|
|
2084
|
-
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
2277
|
+
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
2278
|
+
"./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
|
|
2085
2279
|
});
|
|
2086
2280
|
}
|
|
2087
2281
|
});
|
|
@@ -2130,6 +2324,7 @@ var init_navigators = __esm({
|
|
|
2130
2324
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
2131
2325
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
2132
2326
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
2327
|
+
Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
|
|
2133
2328
|
return Navigators2;
|
|
2134
2329
|
})(Navigators || {});
|
|
2135
2330
|
NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
|
|
@@ -2143,7 +2338,8 @@ var init_navigators = __esm({
|
|
|
2143
2338
|
["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
|
|
2144
2339
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
2145
2340
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
2146
|
-
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER
|
|
2341
|
+
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
2342
|
+
["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
|
|
2147
2343
|
};
|
|
2148
2344
|
ContentNavigator = class {
|
|
2149
2345
|
/** User interface for this navigation session */
|
|
@@ -2168,6 +2364,52 @@ var init_navigators = __esm({
|
|
|
2168
2364
|
this.strategyId = strategyData._id;
|
|
2169
2365
|
}
|
|
2170
2366
|
}
|
|
2367
|
+
// ============================================================================
|
|
2368
|
+
// STRATEGY STATE HELPERS
|
|
2369
|
+
// ============================================================================
|
|
2370
|
+
//
|
|
2371
|
+
// These methods allow strategies to persist their own state (user preferences,
|
|
2372
|
+
// learned patterns, temporal tracking) in the user database.
|
|
2373
|
+
//
|
|
2374
|
+
// ============================================================================
|
|
2375
|
+
/**
|
|
2376
|
+
* Unique key identifying this strategy for state storage.
|
|
2377
|
+
*
|
|
2378
|
+
* Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
|
|
2379
|
+
* Override in subclasses if multiple instances of the same strategy type
|
|
2380
|
+
* need separate state storage.
|
|
2381
|
+
*/
|
|
2382
|
+
get strategyKey() {
|
|
2383
|
+
return this.constructor.name;
|
|
2384
|
+
}
|
|
2385
|
+
/**
|
|
2386
|
+
* Get this strategy's persisted state for the current course.
|
|
2387
|
+
*
|
|
2388
|
+
* @returns The strategy's data payload, or null if no state exists
|
|
2389
|
+
* @throws Error if user or course is not initialized
|
|
2390
|
+
*/
|
|
2391
|
+
async getStrategyState() {
|
|
2392
|
+
if (!this.user || !this.course) {
|
|
2393
|
+
throw new Error(
|
|
2394
|
+
`Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2395
|
+
);
|
|
2396
|
+
}
|
|
2397
|
+
return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
|
|
2398
|
+
}
|
|
2399
|
+
/**
|
|
2400
|
+
* Persist this strategy's state for the current course.
|
|
2401
|
+
*
|
|
2402
|
+
* @param data - The strategy's data payload to store
|
|
2403
|
+
* @throws Error if user or course is not initialized
|
|
2404
|
+
*/
|
|
2405
|
+
async putStrategyState(data) {
|
|
2406
|
+
if (!this.user || !this.course) {
|
|
2407
|
+
throw new Error(
|
|
2408
|
+
`Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2409
|
+
);
|
|
2410
|
+
}
|
|
2411
|
+
return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
|
|
2412
|
+
}
|
|
2171
2413
|
/**
|
|
2172
2414
|
* Factory method to create navigator instances dynamically.
|
|
2173
2415
|
*
|
|
@@ -2584,15 +2826,6 @@ var init_courseDB = __esm({
|
|
|
2584
2826
|
ret[r.id] = r.doc.id_displayable_data;
|
|
2585
2827
|
}
|
|
2586
2828
|
});
|
|
2587
|
-
await Promise.all(
|
|
2588
|
-
cards.rows.map((r) => {
|
|
2589
|
-
return async () => {
|
|
2590
|
-
if (isSuccessRow(r)) {
|
|
2591
|
-
ret[r.id] = r.doc.id_displayable_data;
|
|
2592
|
-
}
|
|
2593
|
-
};
|
|
2594
|
-
})
|
|
2595
|
-
);
|
|
2596
2829
|
return ret;
|
|
2597
2830
|
}
|
|
2598
2831
|
async getCardsByELO(elo, cardLimit) {
|
|
@@ -2677,6 +2910,28 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
2677
2910
|
throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
|
|
2678
2911
|
}
|
|
2679
2912
|
}
|
|
2913
|
+
async getAppliedTagsBatch(cardIds) {
|
|
2914
|
+
if (cardIds.length === 0) {
|
|
2915
|
+
return /* @__PURE__ */ new Map();
|
|
2916
|
+
}
|
|
2917
|
+
const db = getCourseDB2(this.id);
|
|
2918
|
+
const result = await db.query("getTags", {
|
|
2919
|
+
keys: cardIds,
|
|
2920
|
+
include_docs: false
|
|
2921
|
+
});
|
|
2922
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
2923
|
+
for (const cardId of cardIds) {
|
|
2924
|
+
tagsByCard.set(cardId, []);
|
|
2925
|
+
}
|
|
2926
|
+
for (const row of result.rows) {
|
|
2927
|
+
const cardId = row.key;
|
|
2928
|
+
const tagName = row.value?.name;
|
|
2929
|
+
if (tagName && tagsByCard.has(cardId)) {
|
|
2930
|
+
tagsByCard.get(cardId).push(tagName);
|
|
2931
|
+
}
|
|
2932
|
+
}
|
|
2933
|
+
return tagsByCard;
|
|
2934
|
+
}
|
|
2680
2935
|
async addTagToCard(cardId, tagId, updateELO) {
|
|
2681
2936
|
return await addTagToCard(
|
|
2682
2937
|
this.id,
|
|
@@ -3601,6 +3856,16 @@ var init_user = __esm({
|
|
|
3601
3856
|
}
|
|
3602
3857
|
});
|
|
3603
3858
|
|
|
3859
|
+
// src/core/types/strategyState.ts
|
|
3860
|
+
function buildStrategyStateId(courseId, strategyKey) {
|
|
3861
|
+
return `STRATEGY_STATE::${courseId}::${strategyKey}`;
|
|
3862
|
+
}
|
|
3863
|
+
var init_strategyState = __esm({
|
|
3864
|
+
"src/core/types/strategyState.ts"() {
|
|
3865
|
+
"use strict";
|
|
3866
|
+
}
|
|
3867
|
+
});
|
|
3868
|
+
|
|
3604
3869
|
// src/core/util/index.ts
|
|
3605
3870
|
function getCardHistoryID(courseID, cardID) {
|
|
3606
3871
|
return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
|
|
@@ -3644,6 +3909,7 @@ var init_core = __esm({
|
|
|
3644
3909
|
init_interfaces();
|
|
3645
3910
|
init_types_legacy();
|
|
3646
3911
|
init_user();
|
|
3912
|
+
init_strategyState();
|
|
3647
3913
|
init_Loggable();
|
|
3648
3914
|
init_util();
|
|
3649
3915
|
init_navigators();
|
|
@@ -3832,7 +4098,9 @@ import moment5 from "moment";
|
|
|
3832
4098
|
function accomodateGuest() {
|
|
3833
4099
|
logger.log("[funnel] accomodateGuest() called");
|
|
3834
4100
|
if (typeof localStorage === "undefined") {
|
|
3835
|
-
logger.log(
|
|
4101
|
+
logger.log(
|
|
4102
|
+
"[funnel] localStorage not available (Node.js environment), returning default guest"
|
|
4103
|
+
);
|
|
3836
4104
|
return {
|
|
3837
4105
|
username: GuestUsername + "nodejs-test",
|
|
3838
4106
|
firstVisit: true
|
|
@@ -4810,6 +5078,55 @@ Currently logged-in as ${this._username}.`
|
|
|
4810
5078
|
async updateUserElo(courseId, elo) {
|
|
4811
5079
|
return updateUserElo(this._username, courseId, elo);
|
|
4812
5080
|
}
|
|
5081
|
+
async getStrategyState(courseId, strategyKey) {
|
|
5082
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
5083
|
+
try {
|
|
5084
|
+
const doc = await this.localDB.get(docId);
|
|
5085
|
+
return doc.data;
|
|
5086
|
+
} catch (e) {
|
|
5087
|
+
const err = e;
|
|
5088
|
+
if (err.status === 404) {
|
|
5089
|
+
return null;
|
|
5090
|
+
}
|
|
5091
|
+
throw e;
|
|
5092
|
+
}
|
|
5093
|
+
}
|
|
5094
|
+
async putStrategyState(courseId, strategyKey, data) {
|
|
5095
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
5096
|
+
let existingRev;
|
|
5097
|
+
try {
|
|
5098
|
+
const existing = await this.localDB.get(docId);
|
|
5099
|
+
existingRev = existing._rev;
|
|
5100
|
+
} catch (e) {
|
|
5101
|
+
const err = e;
|
|
5102
|
+
if (err.status !== 404) {
|
|
5103
|
+
throw e;
|
|
5104
|
+
}
|
|
5105
|
+
}
|
|
5106
|
+
const doc = {
|
|
5107
|
+
_id: docId,
|
|
5108
|
+
_rev: existingRev,
|
|
5109
|
+
docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
|
|
5110
|
+
courseId,
|
|
5111
|
+
strategyKey,
|
|
5112
|
+
data,
|
|
5113
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5114
|
+
};
|
|
5115
|
+
await this.localDB.put(doc);
|
|
5116
|
+
}
|
|
5117
|
+
async deleteStrategyState(courseId, strategyKey) {
|
|
5118
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
5119
|
+
try {
|
|
5120
|
+
const doc = await this.localDB.get(docId);
|
|
5121
|
+
await this.localDB.remove(doc);
|
|
5122
|
+
} catch (e) {
|
|
5123
|
+
const err = e;
|
|
5124
|
+
if (err.status === 404) {
|
|
5125
|
+
return;
|
|
5126
|
+
}
|
|
5127
|
+
throw e;
|
|
5128
|
+
}
|
|
5129
|
+
}
|
|
4813
5130
|
};
|
|
4814
5131
|
userCoursesDoc = "CourseRegistrations";
|
|
4815
5132
|
userClassroomsDoc = "ClassroomRegistrations";
|
|
@@ -4910,8 +5227,7 @@ var init_adminDB2 = __esm({
|
|
|
4910
5227
|
}
|
|
4911
5228
|
}
|
|
4912
5229
|
}
|
|
4913
|
-
|
|
4914
|
-
return dbs.map((db) => {
|
|
5230
|
+
return promisedCRDbs.map((db) => {
|
|
4915
5231
|
return {
|
|
4916
5232
|
...db.getConfig(),
|
|
4917
5233
|
_id: db._id
|