@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
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionNewItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as StudySessionReviewItem, g as ScheduledCard } from '../../classroomDB-
|
|
2
|
-
import { D as DataLayerProvider } from '../../dataLayerProvider-
|
|
3
|
-
import { S as StaticCourseManifest } from '../../types-
|
|
1
|
+
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionNewItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as StudySessionReviewItem, g as ScheduledCard } from '../../classroomDB-PxDZTky3.cjs';
|
|
2
|
+
import { D as DataLayerProvider } from '../../dataLayerProvider-D0MoZMjH.cjs';
|
|
3
|
+
import { S as StaticCourseManifest } from '../../types-Bn0itutr.cjs';
|
|
4
4
|
import { CourseConfig, CourseElo, DataShape } from '@vue-skuilder/common';
|
|
5
|
-
import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-
|
|
5
|
+
import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-DDY4N-Uq.cjs';
|
|
6
6
|
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.cjs';
|
|
7
7
|
import 'moment';
|
|
8
8
|
|
|
@@ -141,6 +141,7 @@ declare class StaticCourseDB implements CourseDBInterface {
|
|
|
141
141
|
elo: 'user' | 'random' | number;
|
|
142
142
|
}, filter?: (id: QualifiedCardID) => boolean): Promise<StudySessionNewItem[]>;
|
|
143
143
|
getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>>;
|
|
144
|
+
getAppliedTagsBatch(cardIds: string[]): Promise<Map<string, string[]>>;
|
|
144
145
|
addTagToCard(_cardId: string, _tagId: string): Promise<PouchDB.Core.Response>;
|
|
145
146
|
removeTagFromCard(_cardId: string, _tagId: string): Promise<PouchDB.Core.Response>;
|
|
146
147
|
createTag(_tagName: string): Promise<PouchDB.Core.Response>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionNewItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as StudySessionReviewItem, g as ScheduledCard } from '../../classroomDB-
|
|
2
|
-
import { D as DataLayerProvider } from '../../dataLayerProvider-
|
|
3
|
-
import { S as StaticCourseManifest } from '../../types-
|
|
1
|
+
import { U as UserDBInterface, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface, a as UserDBReader, d as CourseInfo, S as StudySessionNewItem, D as DataLayerResult, e as ContentNavigationStrategyData, f as StudySessionReviewItem, g as ScheduledCard } from '../../classroomDB-CZdMBiTU.js';
|
|
2
|
+
import { D as DataLayerProvider } from '../../dataLayerProvider-D8o6ZnKW.js';
|
|
3
|
+
import { S as StaticCourseManifest } from '../../types-DQaXnuoc.js';
|
|
4
4
|
import { CourseConfig, CourseElo, DataShape } from '@vue-skuilder/common';
|
|
5
|
-
import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-
|
|
5
|
+
import { S as SkuilderCourseData, Q as QualifiedCardID, T as TagStub, a as Tag } from '../../types-legacy-DDY4N-Uq.js';
|
|
6
6
|
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.js';
|
|
7
7
|
import 'moment';
|
|
8
8
|
|
|
@@ -141,6 +141,7 @@ declare class StaticCourseDB implements CourseDBInterface {
|
|
|
141
141
|
elo: 'user' | 'random' | number;
|
|
142
142
|
}, filter?: (id: QualifiedCardID) => boolean): Promise<StudySessionNewItem[]>;
|
|
143
143
|
getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>>;
|
|
144
|
+
getAppliedTagsBatch(cardIds: string[]): Promise<Map<string, string[]>>;
|
|
144
145
|
addTagToCard(_cardId: string, _tagId: string): Promise<PouchDB.Core.Response>;
|
|
145
146
|
removeTagFromCard(_cardId: string, _tagId: string): Promise<PouchDB.Core.Response>;
|
|
146
147
|
createTag(_tagName: string): Promise<PouchDB.Core.Response>;
|
|
@@ -119,7 +119,8 @@ var init_types_legacy = __esm({
|
|
|
119
119
|
["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
|
|
120
120
|
["VIEW" /* VIEW */]: "VIEW",
|
|
121
121
|
["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
|
|
122
|
-
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
|
|
122
|
+
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY",
|
|
123
|
+
["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE"
|
|
123
124
|
};
|
|
124
125
|
}
|
|
125
126
|
});
|
|
@@ -688,6 +689,41 @@ var Pipeline_exports = {};
|
|
|
688
689
|
__export(Pipeline_exports, {
|
|
689
690
|
Pipeline: () => Pipeline
|
|
690
691
|
});
|
|
692
|
+
function logPipelineConfig(generator, filters) {
|
|
693
|
+
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
694
|
+
logger.info(
|
|
695
|
+
`[Pipeline] Configuration:
|
|
696
|
+
Generator: ${generator.name}
|
|
697
|
+
Filters:${filterList}`
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
function logTagHydration(cards, tagsByCard) {
|
|
701
|
+
const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
|
|
702
|
+
const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
|
|
703
|
+
logger.debug(
|
|
704
|
+
`[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
|
|
708
|
+
const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
|
|
709
|
+
logger.info(
|
|
710
|
+
`[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
function logCardProvenance(cards, maxCards = 3) {
|
|
714
|
+
const cardsToLog = cards.slice(0, maxCards);
|
|
715
|
+
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
716
|
+
for (const card of cardsToLog) {
|
|
717
|
+
logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
|
|
718
|
+
for (const entry of card.provenance) {
|
|
719
|
+
const scoreChange = entry.score.toFixed(3);
|
|
720
|
+
const action = entry.action.padEnd(9);
|
|
721
|
+
logger.debug(
|
|
722
|
+
`[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
691
727
|
var import_common5, Pipeline;
|
|
692
728
|
var init_Pipeline = __esm({
|
|
693
729
|
"src/core/navigators/Pipeline.ts"() {
|
|
@@ -712,19 +748,18 @@ var init_Pipeline = __esm({
|
|
|
712
748
|
this.filters = filters;
|
|
713
749
|
this.user = user;
|
|
714
750
|
this.course = course;
|
|
715
|
-
|
|
716
|
-
`[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
|
|
717
|
-
);
|
|
751
|
+
logPipelineConfig(generator, filters);
|
|
718
752
|
}
|
|
719
753
|
/**
|
|
720
754
|
* Get weighted cards by running generator and applying filters.
|
|
721
755
|
*
|
|
722
756
|
* 1. Build shared context (user ELO, etc.)
|
|
723
757
|
* 2. Get candidates from generator (passing context)
|
|
724
|
-
* 3.
|
|
725
|
-
* 4.
|
|
726
|
-
* 5.
|
|
727
|
-
* 6.
|
|
758
|
+
* 3. Batch hydrate tags for all candidates
|
|
759
|
+
* 4. Apply each filter sequentially
|
|
760
|
+
* 5. Remove zero-score cards
|
|
761
|
+
* 6. Sort by score descending
|
|
762
|
+
* 7. Return top N
|
|
728
763
|
*
|
|
729
764
|
* @param limit - Maximum number of cards to return
|
|
730
765
|
* @returns Cards sorted by score descending
|
|
@@ -737,7 +772,9 @@ var init_Pipeline = __esm({
|
|
|
737
772
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
738
773
|
);
|
|
739
774
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
740
|
-
|
|
775
|
+
const generatedCount = cards.length;
|
|
776
|
+
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
777
|
+
cards = await this.hydrateTags(cards);
|
|
741
778
|
for (const filter of this.filters) {
|
|
742
779
|
const beforeCount = cards.length;
|
|
743
780
|
cards = await filter.transform(cards, context);
|
|
@@ -746,11 +783,33 @@ var init_Pipeline = __esm({
|
|
|
746
783
|
cards = cards.filter((c) => c.score > 0);
|
|
747
784
|
cards.sort((a, b) => b.score - a.score);
|
|
748
785
|
const result = cards.slice(0, limit);
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
);
|
|
786
|
+
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
787
|
+
logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
|
|
788
|
+
logCardProvenance(result, 3);
|
|
752
789
|
return result;
|
|
753
790
|
}
|
|
791
|
+
/**
|
|
792
|
+
* Batch hydrate tags for all cards.
|
|
793
|
+
*
|
|
794
|
+
* Fetches tags for all cards in a single database query and attaches them
|
|
795
|
+
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
796
|
+
* making individual getAppliedTags() calls.
|
|
797
|
+
*
|
|
798
|
+
* @param cards - Cards to hydrate
|
|
799
|
+
* @returns Cards with tags populated
|
|
800
|
+
*/
|
|
801
|
+
async hydrateTags(cards) {
|
|
802
|
+
if (cards.length === 0) {
|
|
803
|
+
return cards;
|
|
804
|
+
}
|
|
805
|
+
const cardIds = cards.map((c) => c.cardId);
|
|
806
|
+
const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
|
|
807
|
+
logTagHydration(cards, tagsByCard);
|
|
808
|
+
return cards.map((card) => ({
|
|
809
|
+
...card,
|
|
810
|
+
tags: tagsByCard.get(card.cardId) ?? []
|
|
811
|
+
}));
|
|
812
|
+
}
|
|
754
813
|
/**
|
|
755
814
|
* Build shared context for generator and filters.
|
|
756
815
|
*
|
|
@@ -1110,15 +1169,144 @@ var init_eloDistance = __esm({
|
|
|
1110
1169
|
}
|
|
1111
1170
|
});
|
|
1112
1171
|
|
|
1172
|
+
// src/core/navigators/filters/userTagPreference.ts
|
|
1173
|
+
var userTagPreference_exports = {};
|
|
1174
|
+
__export(userTagPreference_exports, {
|
|
1175
|
+
default: () => UserTagPreferenceFilter
|
|
1176
|
+
});
|
|
1177
|
+
var UserTagPreferenceFilter;
|
|
1178
|
+
var init_userTagPreference = __esm({
|
|
1179
|
+
"src/core/navigators/filters/userTagPreference.ts"() {
|
|
1180
|
+
"use strict";
|
|
1181
|
+
init_navigators();
|
|
1182
|
+
UserTagPreferenceFilter = class extends ContentNavigator {
|
|
1183
|
+
_strategyData;
|
|
1184
|
+
/** Human-readable name for CardFilter interface */
|
|
1185
|
+
name;
|
|
1186
|
+
constructor(user, course, strategyData) {
|
|
1187
|
+
super(user, course, strategyData);
|
|
1188
|
+
this._strategyData = strategyData;
|
|
1189
|
+
this.name = strategyData.name || "User Tag Preferences";
|
|
1190
|
+
}
|
|
1191
|
+
/**
|
|
1192
|
+
* Compute multiplier for a card based on its tags and user preferences.
|
|
1193
|
+
* Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
|
|
1194
|
+
*/
|
|
1195
|
+
computeMultiplier(cardTags, boostMap) {
|
|
1196
|
+
const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
|
|
1197
|
+
if (multipliers.length === 0) {
|
|
1198
|
+
return 1;
|
|
1199
|
+
}
|
|
1200
|
+
return Math.max(...multipliers);
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Build human-readable reason for the filter's decision.
|
|
1204
|
+
*/
|
|
1205
|
+
buildReason(cardTags, boostMap, multiplier) {
|
|
1206
|
+
const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
|
|
1207
|
+
if (multiplier === 0) {
|
|
1208
|
+
return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
|
|
1209
|
+
}
|
|
1210
|
+
if (multiplier < 1) {
|
|
1211
|
+
return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1212
|
+
}
|
|
1213
|
+
if (multiplier > 1) {
|
|
1214
|
+
return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1215
|
+
}
|
|
1216
|
+
return "No matching user preferences";
|
|
1217
|
+
}
|
|
1218
|
+
/**
|
|
1219
|
+
* CardFilter.transform implementation.
|
|
1220
|
+
*
|
|
1221
|
+
* Apply user tag preferences:
|
|
1222
|
+
* 1. Read preferences from strategy state
|
|
1223
|
+
* 2. If no preferences, pass through unchanged
|
|
1224
|
+
* 3. For each card:
|
|
1225
|
+
* - Look up tag in boost record
|
|
1226
|
+
* - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
|
|
1227
|
+
* - If multiple tags match: use max multiplier
|
|
1228
|
+
* - Append provenance with clear reason
|
|
1229
|
+
*/
|
|
1230
|
+
async transform(cards, _context) {
|
|
1231
|
+
const prefs = await this.getStrategyState();
|
|
1232
|
+
if (!prefs || Object.keys(prefs.boost).length === 0) {
|
|
1233
|
+
return cards.map((card) => ({
|
|
1234
|
+
...card,
|
|
1235
|
+
provenance: [
|
|
1236
|
+
...card.provenance,
|
|
1237
|
+
{
|
|
1238
|
+
strategy: "userTagPreference",
|
|
1239
|
+
strategyName: this.strategyName || this.name,
|
|
1240
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1241
|
+
action: "passed",
|
|
1242
|
+
score: card.score,
|
|
1243
|
+
reason: "No user tag preferences configured"
|
|
1244
|
+
}
|
|
1245
|
+
]
|
|
1246
|
+
}));
|
|
1247
|
+
}
|
|
1248
|
+
const adjusted = await Promise.all(
|
|
1249
|
+
cards.map(async (card) => {
|
|
1250
|
+
const cardTags = card.tags ?? [];
|
|
1251
|
+
const multiplier = this.computeMultiplier(cardTags, prefs.boost);
|
|
1252
|
+
const finalScore = Math.min(1, card.score * multiplier);
|
|
1253
|
+
let action;
|
|
1254
|
+
if (multiplier === 0 || multiplier < 1) {
|
|
1255
|
+
action = "penalized";
|
|
1256
|
+
} else if (multiplier > 1) {
|
|
1257
|
+
action = "boosted";
|
|
1258
|
+
} else {
|
|
1259
|
+
action = "passed";
|
|
1260
|
+
}
|
|
1261
|
+
return {
|
|
1262
|
+
...card,
|
|
1263
|
+
score: finalScore,
|
|
1264
|
+
provenance: [
|
|
1265
|
+
...card.provenance,
|
|
1266
|
+
{
|
|
1267
|
+
strategy: "userTagPreference",
|
|
1268
|
+
strategyName: this.strategyName || this.name,
|
|
1269
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1270
|
+
action,
|
|
1271
|
+
score: finalScore,
|
|
1272
|
+
reason: this.buildReason(cardTags, prefs.boost, multiplier)
|
|
1273
|
+
}
|
|
1274
|
+
]
|
|
1275
|
+
};
|
|
1276
|
+
})
|
|
1277
|
+
);
|
|
1278
|
+
return adjusted;
|
|
1279
|
+
}
|
|
1280
|
+
/**
|
|
1281
|
+
* Legacy getWeightedCards - throws as filters should not be used as generators.
|
|
1282
|
+
*/
|
|
1283
|
+
async getWeightedCards(_limit) {
|
|
1284
|
+
throw new Error(
|
|
1285
|
+
"UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
|
|
1286
|
+
);
|
|
1287
|
+
}
|
|
1288
|
+
// Legacy methods - stub implementations since filters don't generate cards
|
|
1289
|
+
async getNewCards(_n) {
|
|
1290
|
+
return [];
|
|
1291
|
+
}
|
|
1292
|
+
async getPendingReviews() {
|
|
1293
|
+
return [];
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1113
1299
|
// src/core/navigators/filters/index.ts
|
|
1114
1300
|
var filters_exports = {};
|
|
1115
1301
|
__export(filters_exports, {
|
|
1302
|
+
UserTagPreferenceFilter: () => UserTagPreferenceFilter,
|
|
1116
1303
|
createEloDistanceFilter: () => createEloDistanceFilter
|
|
1117
1304
|
});
|
|
1118
1305
|
var init_filters = __esm({
|
|
1119
1306
|
"src/core/navigators/filters/index.ts"() {
|
|
1120
1307
|
"use strict";
|
|
1121
1308
|
init_eloDistance();
|
|
1309
|
+
init_userTagPreference();
|
|
1122
1310
|
}
|
|
1123
1311
|
});
|
|
1124
1312
|
|
|
@@ -1351,10 +1539,9 @@ var init_hierarchyDefinition = __esm({
|
|
|
1351
1539
|
/**
|
|
1352
1540
|
* Check if a card is unlocked and generate reason.
|
|
1353
1541
|
*/
|
|
1354
|
-
async checkCardUnlock(
|
|
1542
|
+
async checkCardUnlock(card, course, unlockedTags, masteredTags) {
|
|
1355
1543
|
try {
|
|
1356
|
-
const
|
|
1357
|
-
const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
|
|
1544
|
+
const cardTags = card.tags ?? [];
|
|
1358
1545
|
const lockedTags = cardTags.filter(
|
|
1359
1546
|
(tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
|
|
1360
1547
|
);
|
|
@@ -1391,7 +1578,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1391
1578
|
const gated = [];
|
|
1392
1579
|
for (const card of cards) {
|
|
1393
1580
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
1394
|
-
card
|
|
1581
|
+
card,
|
|
1395
1582
|
context.course,
|
|
1396
1583
|
unlockedTags,
|
|
1397
1584
|
masteredTags
|
|
@@ -1437,6 +1624,19 @@ var init_hierarchyDefinition = __esm({
|
|
|
1437
1624
|
}
|
|
1438
1625
|
});
|
|
1439
1626
|
|
|
1627
|
+
// src/core/navigators/inferredPreference.ts
|
|
1628
|
+
var inferredPreference_exports = {};
|
|
1629
|
+
__export(inferredPreference_exports, {
|
|
1630
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
|
|
1631
|
+
});
|
|
1632
|
+
var INFERRED_PREFERENCE_NAVIGATOR_STUB;
|
|
1633
|
+
var init_inferredPreference = __esm({
|
|
1634
|
+
"src/core/navigators/inferredPreference.ts"() {
|
|
1635
|
+
"use strict";
|
|
1636
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
|
|
1440
1640
|
// src/core/navigators/interferenceMitigator.ts
|
|
1441
1641
|
var interferenceMitigator_exports = {};
|
|
1442
1642
|
__export(interferenceMitigator_exports, {
|
|
@@ -1568,17 +1768,6 @@ var init_interferenceMitigator = __esm({
|
|
|
1568
1768
|
}
|
|
1569
1769
|
return avoid;
|
|
1570
1770
|
}
|
|
1571
|
-
/**
|
|
1572
|
-
* Get tags for a single card
|
|
1573
|
-
*/
|
|
1574
|
-
async getCardTags(cardId, course) {
|
|
1575
|
-
try {
|
|
1576
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
1577
|
-
return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
|
|
1578
|
-
} catch {
|
|
1579
|
-
return [];
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
1771
|
/**
|
|
1583
1772
|
* Compute interference score reduction for a card.
|
|
1584
1773
|
* Returns: { multiplier, interfering tags, reason }
|
|
@@ -1630,7 +1819,7 @@ var init_interferenceMitigator = __esm({
|
|
|
1630
1819
|
const tagsToAvoid = this.getTagsToAvoid(immatureTags);
|
|
1631
1820
|
const adjusted = [];
|
|
1632
1821
|
for (const card of cards) {
|
|
1633
|
-
const cardTags =
|
|
1822
|
+
const cardTags = card.tags ?? [];
|
|
1634
1823
|
const { multiplier, reason } = this.computeInterferenceEffect(
|
|
1635
1824
|
cardTags,
|
|
1636
1825
|
tagsToAvoid,
|
|
@@ -1775,27 +1964,16 @@ var init_relativePriority = __esm({
|
|
|
1775
1964
|
return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
|
|
1776
1965
|
}
|
|
1777
1966
|
}
|
|
1778
|
-
/**
|
|
1779
|
-
* Get tags for a single card.
|
|
1780
|
-
*/
|
|
1781
|
-
async getCardTags(cardId, course) {
|
|
1782
|
-
try {
|
|
1783
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
1784
|
-
return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
|
|
1785
|
-
} catch {
|
|
1786
|
-
return [];
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
1967
|
/**
|
|
1790
1968
|
* CardFilter.transform implementation.
|
|
1791
1969
|
*
|
|
1792
1970
|
* Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
|
|
1793
1971
|
* cards with low-priority tags get reduced scores.
|
|
1794
1972
|
*/
|
|
1795
|
-
async transform(cards,
|
|
1973
|
+
async transform(cards, _context) {
|
|
1796
1974
|
const adjusted = await Promise.all(
|
|
1797
1975
|
cards.map(async (card) => {
|
|
1798
|
-
const cardTags =
|
|
1976
|
+
const cardTags = card.tags ?? [];
|
|
1799
1977
|
const priority = this.computeCardPriority(cardTags);
|
|
1800
1978
|
const boostFactor = this.computeBoostFactor(priority);
|
|
1801
1979
|
const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
|
|
@@ -1962,6 +2140,19 @@ var init_srs = __esm({
|
|
|
1962
2140
|
}
|
|
1963
2141
|
});
|
|
1964
2142
|
|
|
2143
|
+
// src/core/navigators/userGoal.ts
|
|
2144
|
+
var userGoal_exports = {};
|
|
2145
|
+
__export(userGoal_exports, {
|
|
2146
|
+
USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
|
|
2147
|
+
});
|
|
2148
|
+
var USER_GOAL_NAVIGATOR_STUB;
|
|
2149
|
+
var init_userGoal = __esm({
|
|
2150
|
+
"src/core/navigators/userGoal.ts"() {
|
|
2151
|
+
"use strict";
|
|
2152
|
+
USER_GOAL_NAVIGATOR_STUB = true;
|
|
2153
|
+
}
|
|
2154
|
+
});
|
|
2155
|
+
|
|
1965
2156
|
// import("./**/*") in src/core/navigators/index.ts
|
|
1966
2157
|
var globImport;
|
|
1967
2158
|
var init_ = __esm({
|
|
@@ -1974,14 +2165,17 @@ var init_ = __esm({
|
|
|
1974
2165
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
1975
2166
|
"./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
|
|
1976
2167
|
"./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
2168
|
+
"./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
|
|
1977
2169
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
1978
2170
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
|
|
1979
2171
|
"./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
|
|
1980
2172
|
"./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
1981
2173
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
|
|
2174
|
+
"./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
|
|
1982
2175
|
"./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
|
|
1983
2176
|
"./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
|
|
1984
|
-
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
2177
|
+
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
2178
|
+
"./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
|
|
1985
2179
|
});
|
|
1986
2180
|
}
|
|
1987
2181
|
});
|
|
@@ -2030,6 +2224,7 @@ var init_navigators = __esm({
|
|
|
2030
2224
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
2031
2225
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
2032
2226
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
2227
|
+
Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
|
|
2033
2228
|
return Navigators2;
|
|
2034
2229
|
})(Navigators || {});
|
|
2035
2230
|
NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
|
|
@@ -2043,7 +2238,8 @@ var init_navigators = __esm({
|
|
|
2043
2238
|
["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
|
|
2044
2239
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
2045
2240
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
2046
|
-
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER
|
|
2241
|
+
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
2242
|
+
["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
|
|
2047
2243
|
};
|
|
2048
2244
|
ContentNavigator = class {
|
|
2049
2245
|
/** User interface for this navigation session */
|
|
@@ -2068,6 +2264,52 @@ var init_navigators = __esm({
|
|
|
2068
2264
|
this.strategyId = strategyData._id;
|
|
2069
2265
|
}
|
|
2070
2266
|
}
|
|
2267
|
+
// ============================================================================
|
|
2268
|
+
// STRATEGY STATE HELPERS
|
|
2269
|
+
// ============================================================================
|
|
2270
|
+
//
|
|
2271
|
+
// These methods allow strategies to persist their own state (user preferences,
|
|
2272
|
+
// learned patterns, temporal tracking) in the user database.
|
|
2273
|
+
//
|
|
2274
|
+
// ============================================================================
|
|
2275
|
+
/**
|
|
2276
|
+
* Unique key identifying this strategy for state storage.
|
|
2277
|
+
*
|
|
2278
|
+
* Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
|
|
2279
|
+
* Override in subclasses if multiple instances of the same strategy type
|
|
2280
|
+
* need separate state storage.
|
|
2281
|
+
*/
|
|
2282
|
+
get strategyKey() {
|
|
2283
|
+
return this.constructor.name;
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Get this strategy's persisted state for the current course.
|
|
2287
|
+
*
|
|
2288
|
+
* @returns The strategy's data payload, or null if no state exists
|
|
2289
|
+
* @throws Error if user or course is not initialized
|
|
2290
|
+
*/
|
|
2291
|
+
async getStrategyState() {
|
|
2292
|
+
if (!this.user || !this.course) {
|
|
2293
|
+
throw new Error(
|
|
2294
|
+
`Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2295
|
+
);
|
|
2296
|
+
}
|
|
2297
|
+
return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
|
|
2298
|
+
}
|
|
2299
|
+
/**
|
|
2300
|
+
* Persist this strategy's state for the current course.
|
|
2301
|
+
*
|
|
2302
|
+
* @param data - The strategy's data payload to store
|
|
2303
|
+
* @throws Error if user or course is not initialized
|
|
2304
|
+
*/
|
|
2305
|
+
async putStrategyState(data) {
|
|
2306
|
+
if (!this.user || !this.course) {
|
|
2307
|
+
throw new Error(
|
|
2308
|
+
`Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2309
|
+
);
|
|
2310
|
+
}
|
|
2311
|
+
return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
|
|
2312
|
+
}
|
|
2071
2313
|
/**
|
|
2072
2314
|
* Factory method to create navigator instances dynamically.
|
|
2073
2315
|
*
|
|
@@ -2295,7 +2537,9 @@ var init_couch = __esm({
|
|
|
2295
2537
|
function accomodateGuest() {
|
|
2296
2538
|
logger.log("[funnel] accomodateGuest() called");
|
|
2297
2539
|
if (typeof localStorage === "undefined") {
|
|
2298
|
-
logger.log(
|
|
2540
|
+
logger.log(
|
|
2541
|
+
"[funnel] localStorage not available (Node.js environment), returning default guest"
|
|
2542
|
+
);
|
|
2299
2543
|
return {
|
|
2300
2544
|
username: GuestUsername + "nodejs-test",
|
|
2301
2545
|
firstVisit: true
|
|
@@ -3275,6 +3519,55 @@ Currently logged-in as ${this._username}.`
|
|
|
3275
3519
|
async updateUserElo(courseId, elo) {
|
|
3276
3520
|
return updateUserElo(this._username, courseId, elo);
|
|
3277
3521
|
}
|
|
3522
|
+
async getStrategyState(courseId, strategyKey) {
|
|
3523
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
3524
|
+
try {
|
|
3525
|
+
const doc = await this.localDB.get(docId);
|
|
3526
|
+
return doc.data;
|
|
3527
|
+
} catch (e) {
|
|
3528
|
+
const err = e;
|
|
3529
|
+
if (err.status === 404) {
|
|
3530
|
+
return null;
|
|
3531
|
+
}
|
|
3532
|
+
throw e;
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
async putStrategyState(courseId, strategyKey, data) {
|
|
3536
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
3537
|
+
let existingRev;
|
|
3538
|
+
try {
|
|
3539
|
+
const existing = await this.localDB.get(docId);
|
|
3540
|
+
existingRev = existing._rev;
|
|
3541
|
+
} catch (e) {
|
|
3542
|
+
const err = e;
|
|
3543
|
+
if (err.status !== 404) {
|
|
3544
|
+
throw e;
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
const doc = {
|
|
3548
|
+
_id: docId,
|
|
3549
|
+
_rev: existingRev,
|
|
3550
|
+
docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
|
|
3551
|
+
courseId,
|
|
3552
|
+
strategyKey,
|
|
3553
|
+
data,
|
|
3554
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3555
|
+
};
|
|
3556
|
+
await this.localDB.put(doc);
|
|
3557
|
+
}
|
|
3558
|
+
async deleteStrategyState(courseId, strategyKey) {
|
|
3559
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
3560
|
+
try {
|
|
3561
|
+
const doc = await this.localDB.get(docId);
|
|
3562
|
+
await this.localDB.remove(doc);
|
|
3563
|
+
} catch (e) {
|
|
3564
|
+
const err = e;
|
|
3565
|
+
if (err.status === 404) {
|
|
3566
|
+
return;
|
|
3567
|
+
}
|
|
3568
|
+
throw e;
|
|
3569
|
+
}
|
|
3570
|
+
}
|
|
3278
3571
|
};
|
|
3279
3572
|
userCoursesDoc = "CourseRegistrations";
|
|
3280
3573
|
userClassroomsDoc = "ClassroomRegistrations";
|
|
@@ -3371,6 +3664,16 @@ var init_user = __esm({
|
|
|
3371
3664
|
}
|
|
3372
3665
|
});
|
|
3373
3666
|
|
|
3667
|
+
// src/core/types/strategyState.ts
|
|
3668
|
+
function buildStrategyStateId(courseId, strategyKey) {
|
|
3669
|
+
return `STRATEGY_STATE::${courseId}::${strategyKey}`;
|
|
3670
|
+
}
|
|
3671
|
+
var init_strategyState = __esm({
|
|
3672
|
+
"src/core/types/strategyState.ts"() {
|
|
3673
|
+
"use strict";
|
|
3674
|
+
}
|
|
3675
|
+
});
|
|
3676
|
+
|
|
3374
3677
|
// src/core/bulkImport/cardProcessor.ts
|
|
3375
3678
|
var import_common16;
|
|
3376
3679
|
var init_cardProcessor = __esm({
|
|
@@ -3404,6 +3707,7 @@ var init_core = __esm({
|
|
|
3404
3707
|
init_interfaces();
|
|
3405
3708
|
init_types_legacy();
|
|
3406
3709
|
init_user();
|
|
3710
|
+
init_strategyState();
|
|
3407
3711
|
init_Loggable();
|
|
3408
3712
|
init_util();
|
|
3409
3713
|
init_navigators();
|
|
@@ -3976,6 +4280,14 @@ var init_courseDB3 = __esm({
|
|
|
3976
4280
|
};
|
|
3977
4281
|
}
|
|
3978
4282
|
}
|
|
4283
|
+
async getAppliedTagsBatch(cardIds) {
|
|
4284
|
+
const tagsIndex = await this.unpacker.getTagsIndex();
|
|
4285
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
4286
|
+
for (const cardId of cardIds) {
|
|
4287
|
+
tagsByCard.set(cardId, tagsIndex.byCard[cardId] || []);
|
|
4288
|
+
}
|
|
4289
|
+
return tagsByCard;
|
|
4290
|
+
}
|
|
3979
4291
|
async addTagToCard(_cardId, _tagId) {
|
|
3980
4292
|
throw new Error("Cannot modify tags in static mode");
|
|
3981
4293
|
}
|