@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,4 +1,4 @@
|
|
|
1
|
-
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './classroomDB-
|
|
1
|
+
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './classroomDB-PxDZTky3.cjs';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Main factory interface for data access
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './classroomDB-
|
|
1
|
+
import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './classroomDB-CZdMBiTU.js';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Main factory interface for data access
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-
|
|
1
|
+
import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-DDY4N-Uq.cjs';
|
|
2
2
|
import { Moment } from 'moment';
|
|
3
|
-
import { A as AdminDBInterface, h as AssignedContent, i as StudyContentSource, j as StudentClassroomDBInterface, U as UserDBInterface, f as StudySessionReviewItem, g as ScheduledCard, S as StudySessionNewItem, W as WeightedCard, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, k as ContentNavigator, l as StudySessionItem } from '../../classroomDB-
|
|
4
|
-
export { q as ContentSourceID, m as StudySessionFailedItem, n as StudySessionFailedNewItem, o as StudySessionFailedReviewItem, r as getStudySource, p as isReview } from '../../classroomDB-
|
|
3
|
+
import { A as AdminDBInterface, h as AssignedContent, i as StudyContentSource, j as StudentClassroomDBInterface, U as UserDBInterface, f as StudySessionReviewItem, g as ScheduledCard, S as StudySessionNewItem, W as WeightedCard, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, k as ContentNavigator, l as StudySessionItem } from '../../classroomDB-PxDZTky3.cjs';
|
|
4
|
+
export { q as ContentSourceID, m as StudySessionFailedItem, n as StudySessionFailedNewItem, o as StudySessionFailedReviewItem, r as getStudySource, p as isReview } from '../../classroomDB-PxDZTky3.cjs';
|
|
5
5
|
import * as _vue_skuilder_common from '@vue-skuilder/common';
|
|
6
6
|
import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
|
|
7
7
|
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.cjs';
|
|
@@ -191,6 +191,7 @@ declare class CourseDB implements StudyContentSource, CourseDBInterface {
|
|
|
191
191
|
updateCourseConfig(cfg: CourseConfig$1): Promise<PouchDB.Core.Response>;
|
|
192
192
|
updateCardElo(cardId: string, elo: CourseElo): Promise<PouchDB.Core.Response>;
|
|
193
193
|
getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>>;
|
|
194
|
+
getAppliedTagsBatch(cardIds: string[]): Promise<Map<string, string[]>>;
|
|
194
195
|
addTagToCard(cardId: string, tagId: string, updateELO?: boolean): Promise<PouchDB.Core.Response>;
|
|
195
196
|
removeTagFromCard(cardId: string, tagId: string): Promise<PouchDB.Core.Response>;
|
|
196
197
|
createTag(name: string, author: string): Promise<PouchDB.Core.Response>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-
|
|
1
|
+
import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-DDY4N-Uq.js';
|
|
2
2
|
import { Moment } from 'moment';
|
|
3
|
-
import { A as AdminDBInterface, h as AssignedContent, i as StudyContentSource, j as StudentClassroomDBInterface, U as UserDBInterface, f as StudySessionReviewItem, g as ScheduledCard, S as StudySessionNewItem, W as WeightedCard, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, k as ContentNavigator, l as StudySessionItem } from '../../classroomDB-
|
|
4
|
-
export { q as ContentSourceID, m as StudySessionFailedItem, n as StudySessionFailedNewItem, o as StudySessionFailedReviewItem, r as getStudySource, p as isReview } from '../../classroomDB-
|
|
3
|
+
import { A as AdminDBInterface, h as AssignedContent, i as StudyContentSource, j as StudentClassroomDBInterface, U as UserDBInterface, f as StudySessionReviewItem, g as ScheduledCard, S as StudySessionNewItem, W as WeightedCard, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, k as ContentNavigator, l as StudySessionItem } from '../../classroomDB-CZdMBiTU.js';
|
|
4
|
+
export { q as ContentSourceID, m as StudySessionFailedItem, n as StudySessionFailedNewItem, o as StudySessionFailedReviewItem, r as getStudySource, p as isReview } from '../../classroomDB-CZdMBiTU.js';
|
|
5
5
|
import * as _vue_skuilder_common from '@vue-skuilder/common';
|
|
6
6
|
import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
|
|
7
7
|
import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.js';
|
|
@@ -191,6 +191,7 @@ declare class CourseDB implements StudyContentSource, CourseDBInterface {
|
|
|
191
191
|
updateCourseConfig(cfg: CourseConfig$1): Promise<PouchDB.Core.Response>;
|
|
192
192
|
updateCardElo(cardId: string, elo: CourseElo): Promise<PouchDB.Core.Response>;
|
|
193
193
|
getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>>;
|
|
194
|
+
getAppliedTagsBatch(cardIds: string[]): Promise<Map<string, string[]>>;
|
|
194
195
|
addTagToCard(cardId: string, tagId: string, updateELO?: boolean): Promise<PouchDB.Core.Response>;
|
|
195
196
|
removeTagFromCard(cardId: string, tagId: string): Promise<PouchDB.Core.Response>;
|
|
196
197
|
createTag(name: string, author: string): Promise<PouchDB.Core.Response>;
|
package/dist/impl/couch/index.js
CHANGED
|
@@ -280,7 +280,8 @@ var init_types_legacy = __esm({
|
|
|
280
280
|
["QUESTION" /* QUESTIONTYPE */]: "QUESTION",
|
|
281
281
|
["VIEW" /* VIEW */]: "VIEW",
|
|
282
282
|
["PEDAGOGY" /* PEDAGOGY */]: "PEDAGOGY",
|
|
283
|
-
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY"
|
|
283
|
+
["NAVIGATION_STRATEGY" /* NAVIGATION_STRATEGY */]: "NAVIGATION_STRATEGY",
|
|
284
|
+
["STRATEGY_STATE" /* STRATEGY_STATE */]: "STRATEGY_STATE"
|
|
284
285
|
};
|
|
285
286
|
}
|
|
286
287
|
});
|
|
@@ -810,6 +811,41 @@ var Pipeline_exports = {};
|
|
|
810
811
|
__export(Pipeline_exports, {
|
|
811
812
|
Pipeline: () => Pipeline
|
|
812
813
|
});
|
|
814
|
+
function logPipelineConfig(generator, filters) {
|
|
815
|
+
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
816
|
+
logger.info(
|
|
817
|
+
`[Pipeline] Configuration:
|
|
818
|
+
Generator: ${generator.name}
|
|
819
|
+
Filters:${filterList}`
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
function logTagHydration(cards, tagsByCard) {
|
|
823
|
+
const totalTags = Array.from(tagsByCard.values()).reduce((sum, tags) => sum + tags.length, 0);
|
|
824
|
+
const cardsWithTags = Array.from(tagsByCard.values()).filter((tags) => tags.length > 0).length;
|
|
825
|
+
logger.debug(
|
|
826
|
+
`[Pipeline] Tag hydration: ${cards.length} cards, ${cardsWithTags} have tags (${totalTags} total tags) - single batch query`
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
function logExecutionSummary(generatorName, generatedCount, filterCount, finalCount, topScores) {
|
|
830
|
+
const scoreDisplay = topScores.length > 0 ? topScores.map((s) => s.toFixed(2)).join(", ") : "none";
|
|
831
|
+
logger.info(
|
|
832
|
+
`[Pipeline] Execution: ${generatorName} produced ${generatedCount} \u2192 ${filterCount} filters \u2192 ${finalCount} results (top scores: ${scoreDisplay})`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
function logCardProvenance(cards, maxCards = 3) {
|
|
836
|
+
const cardsToLog = cards.slice(0, maxCards);
|
|
837
|
+
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
838
|
+
for (const card of cardsToLog) {
|
|
839
|
+
logger.debug(`[Pipeline] ${card.cardId} (final score: ${card.score.toFixed(3)}):`);
|
|
840
|
+
for (const entry of card.provenance) {
|
|
841
|
+
const scoreChange = entry.score.toFixed(3);
|
|
842
|
+
const action = entry.action.padEnd(9);
|
|
843
|
+
logger.debug(
|
|
844
|
+
`[Pipeline] ${action} ${scoreChange} - ${entry.strategyName}: ${entry.reason}`
|
|
845
|
+
);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
813
849
|
var import_common5, Pipeline;
|
|
814
850
|
var init_Pipeline = __esm({
|
|
815
851
|
"src/core/navigators/Pipeline.ts"() {
|
|
@@ -834,19 +870,18 @@ var init_Pipeline = __esm({
|
|
|
834
870
|
this.filters = filters;
|
|
835
871
|
this.user = user;
|
|
836
872
|
this.course = course;
|
|
837
|
-
|
|
838
|
-
`[Pipeline] Created with generator '${generator.name}' and ${filters.length} filters: ${filters.map((f) => f.name).join(", ")}`
|
|
839
|
-
);
|
|
873
|
+
logPipelineConfig(generator, filters);
|
|
840
874
|
}
|
|
841
875
|
/**
|
|
842
876
|
* Get weighted cards by running generator and applying filters.
|
|
843
877
|
*
|
|
844
878
|
* 1. Build shared context (user ELO, etc.)
|
|
845
879
|
* 2. Get candidates from generator (passing context)
|
|
846
|
-
* 3.
|
|
847
|
-
* 4.
|
|
848
|
-
* 5.
|
|
849
|
-
* 6.
|
|
880
|
+
* 3. Batch hydrate tags for all candidates
|
|
881
|
+
* 4. Apply each filter sequentially
|
|
882
|
+
* 5. Remove zero-score cards
|
|
883
|
+
* 6. Sort by score descending
|
|
884
|
+
* 7. Return top N
|
|
850
885
|
*
|
|
851
886
|
* @param limit - Maximum number of cards to return
|
|
852
887
|
* @returns Cards sorted by score descending
|
|
@@ -859,7 +894,9 @@ var init_Pipeline = __esm({
|
|
|
859
894
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
860
895
|
);
|
|
861
896
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
862
|
-
|
|
897
|
+
const generatedCount = cards.length;
|
|
898
|
+
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
899
|
+
cards = await this.hydrateTags(cards);
|
|
863
900
|
for (const filter of this.filters) {
|
|
864
901
|
const beforeCount = cards.length;
|
|
865
902
|
cards = await filter.transform(cards, context);
|
|
@@ -868,11 +905,33 @@ var init_Pipeline = __esm({
|
|
|
868
905
|
cards = cards.filter((c) => c.score > 0);
|
|
869
906
|
cards.sort((a, b) => b.score - a.score);
|
|
870
907
|
const result = cards.slice(0, limit);
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
);
|
|
908
|
+
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
909
|
+
logExecutionSummary(this.generator.name, generatedCount, this.filters.length, result.length, topScores);
|
|
910
|
+
logCardProvenance(result, 3);
|
|
874
911
|
return result;
|
|
875
912
|
}
|
|
913
|
+
/**
|
|
914
|
+
* Batch hydrate tags for all cards.
|
|
915
|
+
*
|
|
916
|
+
* Fetches tags for all cards in a single database query and attaches them
|
|
917
|
+
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
918
|
+
* making individual getAppliedTags() calls.
|
|
919
|
+
*
|
|
920
|
+
* @param cards - Cards to hydrate
|
|
921
|
+
* @returns Cards with tags populated
|
|
922
|
+
*/
|
|
923
|
+
async hydrateTags(cards) {
|
|
924
|
+
if (cards.length === 0) {
|
|
925
|
+
return cards;
|
|
926
|
+
}
|
|
927
|
+
const cardIds = cards.map((c) => c.cardId);
|
|
928
|
+
const tagsByCard = await this.course.getAppliedTagsBatch(cardIds);
|
|
929
|
+
logTagHydration(cards, tagsByCard);
|
|
930
|
+
return cards.map((card) => ({
|
|
931
|
+
...card,
|
|
932
|
+
tags: tagsByCard.get(card.cardId) ?? []
|
|
933
|
+
}));
|
|
934
|
+
}
|
|
876
935
|
/**
|
|
877
936
|
* Build shared context for generator and filters.
|
|
878
937
|
*
|
|
@@ -1232,15 +1291,144 @@ var init_eloDistance = __esm({
|
|
|
1232
1291
|
}
|
|
1233
1292
|
});
|
|
1234
1293
|
|
|
1294
|
+
// src/core/navigators/filters/userTagPreference.ts
|
|
1295
|
+
var userTagPreference_exports = {};
|
|
1296
|
+
__export(userTagPreference_exports, {
|
|
1297
|
+
default: () => UserTagPreferenceFilter
|
|
1298
|
+
});
|
|
1299
|
+
var UserTagPreferenceFilter;
|
|
1300
|
+
var init_userTagPreference = __esm({
|
|
1301
|
+
"src/core/navigators/filters/userTagPreference.ts"() {
|
|
1302
|
+
"use strict";
|
|
1303
|
+
init_navigators();
|
|
1304
|
+
UserTagPreferenceFilter = class extends ContentNavigator {
|
|
1305
|
+
_strategyData;
|
|
1306
|
+
/** Human-readable name for CardFilter interface */
|
|
1307
|
+
name;
|
|
1308
|
+
constructor(user, course, strategyData) {
|
|
1309
|
+
super(user, course, strategyData);
|
|
1310
|
+
this._strategyData = strategyData;
|
|
1311
|
+
this.name = strategyData.name || "User Tag Preferences";
|
|
1312
|
+
}
|
|
1313
|
+
/**
|
|
1314
|
+
* Compute multiplier for a card based on its tags and user preferences.
|
|
1315
|
+
* Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
|
|
1316
|
+
*/
|
|
1317
|
+
computeMultiplier(cardTags, boostMap) {
|
|
1318
|
+
const multipliers = cardTags.map((tag) => boostMap[tag]).filter((val) => val !== void 0);
|
|
1319
|
+
if (multipliers.length === 0) {
|
|
1320
|
+
return 1;
|
|
1321
|
+
}
|
|
1322
|
+
return Math.max(...multipliers);
|
|
1323
|
+
}
|
|
1324
|
+
/**
|
|
1325
|
+
* Build human-readable reason for the filter's decision.
|
|
1326
|
+
*/
|
|
1327
|
+
buildReason(cardTags, boostMap, multiplier) {
|
|
1328
|
+
const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
|
|
1329
|
+
if (multiplier === 0) {
|
|
1330
|
+
return `Excluded by user preference: ${matchingTags.join(", ")} (${multiplier}x)`;
|
|
1331
|
+
}
|
|
1332
|
+
if (multiplier < 1) {
|
|
1333
|
+
return `Penalized by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1334
|
+
}
|
|
1335
|
+
if (multiplier > 1) {
|
|
1336
|
+
return `Boosted by user preference: ${matchingTags.join(", ")} (${multiplier.toFixed(2)}x)`;
|
|
1337
|
+
}
|
|
1338
|
+
return "No matching user preferences";
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* CardFilter.transform implementation.
|
|
1342
|
+
*
|
|
1343
|
+
* Apply user tag preferences:
|
|
1344
|
+
* 1. Read preferences from strategy state
|
|
1345
|
+
* 2. If no preferences, pass through unchanged
|
|
1346
|
+
* 3. For each card:
|
|
1347
|
+
* - Look up tag in boost record
|
|
1348
|
+
* - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
|
|
1349
|
+
* - If multiple tags match: use max multiplier
|
|
1350
|
+
* - Append provenance with clear reason
|
|
1351
|
+
*/
|
|
1352
|
+
async transform(cards, _context) {
|
|
1353
|
+
const prefs = await this.getStrategyState();
|
|
1354
|
+
if (!prefs || Object.keys(prefs.boost).length === 0) {
|
|
1355
|
+
return cards.map((card) => ({
|
|
1356
|
+
...card,
|
|
1357
|
+
provenance: [
|
|
1358
|
+
...card.provenance,
|
|
1359
|
+
{
|
|
1360
|
+
strategy: "userTagPreference",
|
|
1361
|
+
strategyName: this.strategyName || this.name,
|
|
1362
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1363
|
+
action: "passed",
|
|
1364
|
+
score: card.score,
|
|
1365
|
+
reason: "No user tag preferences configured"
|
|
1366
|
+
}
|
|
1367
|
+
]
|
|
1368
|
+
}));
|
|
1369
|
+
}
|
|
1370
|
+
const adjusted = await Promise.all(
|
|
1371
|
+
cards.map(async (card) => {
|
|
1372
|
+
const cardTags = card.tags ?? [];
|
|
1373
|
+
const multiplier = this.computeMultiplier(cardTags, prefs.boost);
|
|
1374
|
+
const finalScore = Math.min(1, card.score * multiplier);
|
|
1375
|
+
let action;
|
|
1376
|
+
if (multiplier === 0 || multiplier < 1) {
|
|
1377
|
+
action = "penalized";
|
|
1378
|
+
} else if (multiplier > 1) {
|
|
1379
|
+
action = "boosted";
|
|
1380
|
+
} else {
|
|
1381
|
+
action = "passed";
|
|
1382
|
+
}
|
|
1383
|
+
return {
|
|
1384
|
+
...card,
|
|
1385
|
+
score: finalScore,
|
|
1386
|
+
provenance: [
|
|
1387
|
+
...card.provenance,
|
|
1388
|
+
{
|
|
1389
|
+
strategy: "userTagPreference",
|
|
1390
|
+
strategyName: this.strategyName || this.name,
|
|
1391
|
+
strategyId: this.strategyId || this._strategyData._id,
|
|
1392
|
+
action,
|
|
1393
|
+
score: finalScore,
|
|
1394
|
+
reason: this.buildReason(cardTags, prefs.boost, multiplier)
|
|
1395
|
+
}
|
|
1396
|
+
]
|
|
1397
|
+
};
|
|
1398
|
+
})
|
|
1399
|
+
);
|
|
1400
|
+
return adjusted;
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Legacy getWeightedCards - throws as filters should not be used as generators.
|
|
1404
|
+
*/
|
|
1405
|
+
async getWeightedCards(_limit) {
|
|
1406
|
+
throw new Error(
|
|
1407
|
+
"UserTagPreferenceFilter is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
|
|
1408
|
+
);
|
|
1409
|
+
}
|
|
1410
|
+
// Legacy methods - stub implementations since filters don't generate cards
|
|
1411
|
+
async getNewCards(_n) {
|
|
1412
|
+
return [];
|
|
1413
|
+
}
|
|
1414
|
+
async getPendingReviews() {
|
|
1415
|
+
return [];
|
|
1416
|
+
}
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1235
1421
|
// src/core/navigators/filters/index.ts
|
|
1236
1422
|
var filters_exports = {};
|
|
1237
1423
|
__export(filters_exports, {
|
|
1424
|
+
UserTagPreferenceFilter: () => UserTagPreferenceFilter,
|
|
1238
1425
|
createEloDistanceFilter: () => createEloDistanceFilter
|
|
1239
1426
|
});
|
|
1240
1427
|
var init_filters = __esm({
|
|
1241
1428
|
"src/core/navigators/filters/index.ts"() {
|
|
1242
1429
|
"use strict";
|
|
1243
1430
|
init_eloDistance();
|
|
1431
|
+
init_userTagPreference();
|
|
1244
1432
|
}
|
|
1245
1433
|
});
|
|
1246
1434
|
|
|
@@ -1473,10 +1661,9 @@ var init_hierarchyDefinition = __esm({
|
|
|
1473
1661
|
/**
|
|
1474
1662
|
* Check if a card is unlocked and generate reason.
|
|
1475
1663
|
*/
|
|
1476
|
-
async checkCardUnlock(
|
|
1664
|
+
async checkCardUnlock(card, course, unlockedTags, masteredTags) {
|
|
1477
1665
|
try {
|
|
1478
|
-
const
|
|
1479
|
-
const cardTags = tagResponse.rows.map((row) => row.value?.name || row.key);
|
|
1666
|
+
const cardTags = card.tags ?? [];
|
|
1480
1667
|
const lockedTags = cardTags.filter(
|
|
1481
1668
|
(tag) => this.hasPrerequisites(tag) && !unlockedTags.has(tag)
|
|
1482
1669
|
);
|
|
@@ -1513,7 +1700,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1513
1700
|
const gated = [];
|
|
1514
1701
|
for (const card of cards) {
|
|
1515
1702
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
1516
|
-
card
|
|
1703
|
+
card,
|
|
1517
1704
|
context.course,
|
|
1518
1705
|
unlockedTags,
|
|
1519
1706
|
masteredTags
|
|
@@ -1559,6 +1746,19 @@ var init_hierarchyDefinition = __esm({
|
|
|
1559
1746
|
}
|
|
1560
1747
|
});
|
|
1561
1748
|
|
|
1749
|
+
// src/core/navigators/inferredPreference.ts
|
|
1750
|
+
var inferredPreference_exports = {};
|
|
1751
|
+
__export(inferredPreference_exports, {
|
|
1752
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB: () => INFERRED_PREFERENCE_NAVIGATOR_STUB
|
|
1753
|
+
});
|
|
1754
|
+
var INFERRED_PREFERENCE_NAVIGATOR_STUB;
|
|
1755
|
+
var init_inferredPreference = __esm({
|
|
1756
|
+
"src/core/navigators/inferredPreference.ts"() {
|
|
1757
|
+
"use strict";
|
|
1758
|
+
INFERRED_PREFERENCE_NAVIGATOR_STUB = true;
|
|
1759
|
+
}
|
|
1760
|
+
});
|
|
1761
|
+
|
|
1562
1762
|
// src/core/navigators/interferenceMitigator.ts
|
|
1563
1763
|
var interferenceMitigator_exports = {};
|
|
1564
1764
|
__export(interferenceMitigator_exports, {
|
|
@@ -1690,17 +1890,6 @@ var init_interferenceMitigator = __esm({
|
|
|
1690
1890
|
}
|
|
1691
1891
|
return avoid;
|
|
1692
1892
|
}
|
|
1693
|
-
/**
|
|
1694
|
-
* Get tags for a single card
|
|
1695
|
-
*/
|
|
1696
|
-
async getCardTags(cardId, course) {
|
|
1697
|
-
try {
|
|
1698
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
1699
|
-
return tagResponse.rows.map((row) => row.value?.name || row.key).filter(Boolean);
|
|
1700
|
-
} catch {
|
|
1701
|
-
return [];
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
1893
|
/**
|
|
1705
1894
|
* Compute interference score reduction for a card.
|
|
1706
1895
|
* Returns: { multiplier, interfering tags, reason }
|
|
@@ -1752,7 +1941,7 @@ var init_interferenceMitigator = __esm({
|
|
|
1752
1941
|
const tagsToAvoid = this.getTagsToAvoid(immatureTags);
|
|
1753
1942
|
const adjusted = [];
|
|
1754
1943
|
for (const card of cards) {
|
|
1755
|
-
const cardTags =
|
|
1944
|
+
const cardTags = card.tags ?? [];
|
|
1756
1945
|
const { multiplier, reason } = this.computeInterferenceEffect(
|
|
1757
1946
|
cardTags,
|
|
1758
1947
|
tagsToAvoid,
|
|
@@ -1897,27 +2086,16 @@ var init_relativePriority = __esm({
|
|
|
1897
2086
|
return `Low-priority tags: ${tagList}${more} (priority ${priority.toFixed(2)} \u2192 reduce ${boostFactor.toFixed(2)}x \u2192 ${finalScore.toFixed(2)})`;
|
|
1898
2087
|
}
|
|
1899
2088
|
}
|
|
1900
|
-
/**
|
|
1901
|
-
* Get tags for a single card.
|
|
1902
|
-
*/
|
|
1903
|
-
async getCardTags(cardId, course) {
|
|
1904
|
-
try {
|
|
1905
|
-
const tagResponse = await course.getAppliedTags(cardId);
|
|
1906
|
-
return tagResponse.rows.map((r) => r.doc?.name).filter((x) => !!x);
|
|
1907
|
-
} catch {
|
|
1908
|
-
return [];
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
2089
|
/**
|
|
1912
2090
|
* CardFilter.transform implementation.
|
|
1913
2091
|
*
|
|
1914
2092
|
* Apply priority-adjusted scoring. Cards with high-priority tags get boosted,
|
|
1915
2093
|
* cards with low-priority tags get reduced scores.
|
|
1916
2094
|
*/
|
|
1917
|
-
async transform(cards,
|
|
2095
|
+
async transform(cards, _context) {
|
|
1918
2096
|
const adjusted = await Promise.all(
|
|
1919
2097
|
cards.map(async (card) => {
|
|
1920
|
-
const cardTags =
|
|
2098
|
+
const cardTags = card.tags ?? [];
|
|
1921
2099
|
const priority = this.computeCardPriority(cardTags);
|
|
1922
2100
|
const boostFactor = this.computeBoostFactor(priority);
|
|
1923
2101
|
const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
|
|
@@ -2084,6 +2262,19 @@ var init_srs = __esm({
|
|
|
2084
2262
|
}
|
|
2085
2263
|
});
|
|
2086
2264
|
|
|
2265
|
+
// src/core/navigators/userGoal.ts
|
|
2266
|
+
var userGoal_exports = {};
|
|
2267
|
+
__export(userGoal_exports, {
|
|
2268
|
+
USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
|
|
2269
|
+
});
|
|
2270
|
+
var USER_GOAL_NAVIGATOR_STUB;
|
|
2271
|
+
var init_userGoal = __esm({
|
|
2272
|
+
"src/core/navigators/userGoal.ts"() {
|
|
2273
|
+
"use strict";
|
|
2274
|
+
USER_GOAL_NAVIGATOR_STUB = true;
|
|
2275
|
+
}
|
|
2276
|
+
});
|
|
2277
|
+
|
|
2087
2278
|
// import("./**/*") in src/core/navigators/index.ts
|
|
2088
2279
|
var globImport;
|
|
2089
2280
|
var init_ = __esm({
|
|
@@ -2096,14 +2287,17 @@ var init_ = __esm({
|
|
|
2096
2287
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
2097
2288
|
"./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
|
|
2098
2289
|
"./filters/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
2290
|
+
"./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports)),
|
|
2099
2291
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
2100
2292
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
|
|
2101
2293
|
"./hardcodedOrder.ts": () => Promise.resolve().then(() => (init_hardcodedOrder(), hardcodedOrder_exports)),
|
|
2102
2294
|
"./hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
2103
2295
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports)),
|
|
2296
|
+
"./inferredPreference.ts": () => Promise.resolve().then(() => (init_inferredPreference(), inferredPreference_exports)),
|
|
2104
2297
|
"./interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
|
|
2105
2298
|
"./relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
|
|
2106
|
-
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
2299
|
+
"./srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
2300
|
+
"./userGoal.ts": () => Promise.resolve().then(() => (init_userGoal(), userGoal_exports))
|
|
2107
2301
|
});
|
|
2108
2302
|
}
|
|
2109
2303
|
});
|
|
@@ -2152,6 +2346,7 @@ var init_navigators = __esm({
|
|
|
2152
2346
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
2153
2347
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
2154
2348
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
2349
|
+
Navigators2["USER_TAG_PREFERENCE"] = "userTagPreference";
|
|
2155
2350
|
return Navigators2;
|
|
2156
2351
|
})(Navigators || {});
|
|
2157
2352
|
NavigatorRole = /* @__PURE__ */ ((NavigatorRole2) => {
|
|
@@ -2165,7 +2360,8 @@ var init_navigators = __esm({
|
|
|
2165
2360
|
["hardcodedOrder" /* HARDCODED */]: "generator" /* GENERATOR */,
|
|
2166
2361
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
2167
2362
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
2168
|
-
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER
|
|
2363
|
+
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
2364
|
+
["userTagPreference" /* USER_TAG_PREFERENCE */]: "filter" /* FILTER */
|
|
2169
2365
|
};
|
|
2170
2366
|
ContentNavigator = class {
|
|
2171
2367
|
/** User interface for this navigation session */
|
|
@@ -2190,6 +2386,52 @@ var init_navigators = __esm({
|
|
|
2190
2386
|
this.strategyId = strategyData._id;
|
|
2191
2387
|
}
|
|
2192
2388
|
}
|
|
2389
|
+
// ============================================================================
|
|
2390
|
+
// STRATEGY STATE HELPERS
|
|
2391
|
+
// ============================================================================
|
|
2392
|
+
//
|
|
2393
|
+
// These methods allow strategies to persist their own state (user preferences,
|
|
2394
|
+
// learned patterns, temporal tracking) in the user database.
|
|
2395
|
+
//
|
|
2396
|
+
// ============================================================================
|
|
2397
|
+
/**
|
|
2398
|
+
* Unique key identifying this strategy for state storage.
|
|
2399
|
+
*
|
|
2400
|
+
* Defaults to the constructor name (e.g., "UserTagPreferenceFilter").
|
|
2401
|
+
* Override in subclasses if multiple instances of the same strategy type
|
|
2402
|
+
* need separate state storage.
|
|
2403
|
+
*/
|
|
2404
|
+
get strategyKey() {
|
|
2405
|
+
return this.constructor.name;
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Get this strategy's persisted state for the current course.
|
|
2409
|
+
*
|
|
2410
|
+
* @returns The strategy's data payload, or null if no state exists
|
|
2411
|
+
* @throws Error if user or course is not initialized
|
|
2412
|
+
*/
|
|
2413
|
+
async getStrategyState() {
|
|
2414
|
+
if (!this.user || !this.course) {
|
|
2415
|
+
throw new Error(
|
|
2416
|
+
`Cannot get strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
return this.user.getStrategyState(this.course.getCourseID(), this.strategyKey);
|
|
2420
|
+
}
|
|
2421
|
+
/**
|
|
2422
|
+
* Persist this strategy's state for the current course.
|
|
2423
|
+
*
|
|
2424
|
+
* @param data - The strategy's data payload to store
|
|
2425
|
+
* @throws Error if user or course is not initialized
|
|
2426
|
+
*/
|
|
2427
|
+
async putStrategyState(data) {
|
|
2428
|
+
if (!this.user || !this.course) {
|
|
2429
|
+
throw new Error(
|
|
2430
|
+
`Cannot put strategy state: navigator not properly initialized. Ensure user and course are provided to constructor.`
|
|
2431
|
+
);
|
|
2432
|
+
}
|
|
2433
|
+
return this.user.putStrategyState(this.course.getCourseID(), this.strategyKey, data);
|
|
2434
|
+
}
|
|
2193
2435
|
/**
|
|
2194
2436
|
* Factory method to create navigator instances dynamically.
|
|
2195
2437
|
*
|
|
@@ -2601,15 +2843,6 @@ var init_courseDB = __esm({
|
|
|
2601
2843
|
ret[r.id] = r.doc.id_displayable_data;
|
|
2602
2844
|
}
|
|
2603
2845
|
});
|
|
2604
|
-
await Promise.all(
|
|
2605
|
-
cards.rows.map((r) => {
|
|
2606
|
-
return async () => {
|
|
2607
|
-
if (isSuccessRow(r)) {
|
|
2608
|
-
ret[r.id] = r.doc.id_displayable_data;
|
|
2609
|
-
}
|
|
2610
|
-
};
|
|
2611
|
-
})
|
|
2612
|
-
);
|
|
2613
2846
|
return ret;
|
|
2614
2847
|
}
|
|
2615
2848
|
async getCardsByELO(elo, cardLimit) {
|
|
@@ -2694,6 +2927,28 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
2694
2927
|
throw new Error(`Failed to find tags for card ${this.id}-${cardId}`);
|
|
2695
2928
|
}
|
|
2696
2929
|
}
|
|
2930
|
+
async getAppliedTagsBatch(cardIds) {
|
|
2931
|
+
if (cardIds.length === 0) {
|
|
2932
|
+
return /* @__PURE__ */ new Map();
|
|
2933
|
+
}
|
|
2934
|
+
const db = getCourseDB2(this.id);
|
|
2935
|
+
const result = await db.query("getTags", {
|
|
2936
|
+
keys: cardIds,
|
|
2937
|
+
include_docs: false
|
|
2938
|
+
});
|
|
2939
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
2940
|
+
for (const cardId of cardIds) {
|
|
2941
|
+
tagsByCard.set(cardId, []);
|
|
2942
|
+
}
|
|
2943
|
+
for (const row of result.rows) {
|
|
2944
|
+
const cardId = row.key;
|
|
2945
|
+
const tagName = row.value?.name;
|
|
2946
|
+
if (tagName && tagsByCard.has(cardId)) {
|
|
2947
|
+
tagsByCard.get(cardId).push(tagName);
|
|
2948
|
+
}
|
|
2949
|
+
}
|
|
2950
|
+
return tagsByCard;
|
|
2951
|
+
}
|
|
2697
2952
|
async addTagToCard(cardId, tagId, updateELO) {
|
|
2698
2953
|
return await addTagToCard(
|
|
2699
2954
|
this.id,
|
|
@@ -3619,6 +3874,16 @@ var init_user = __esm({
|
|
|
3619
3874
|
}
|
|
3620
3875
|
});
|
|
3621
3876
|
|
|
3877
|
+
// src/core/types/strategyState.ts
|
|
3878
|
+
function buildStrategyStateId(courseId, strategyKey) {
|
|
3879
|
+
return `STRATEGY_STATE::${courseId}::${strategyKey}`;
|
|
3880
|
+
}
|
|
3881
|
+
var init_strategyState = __esm({
|
|
3882
|
+
"src/core/types/strategyState.ts"() {
|
|
3883
|
+
"use strict";
|
|
3884
|
+
}
|
|
3885
|
+
});
|
|
3886
|
+
|
|
3622
3887
|
// src/core/util/index.ts
|
|
3623
3888
|
function getCardHistoryID(courseID, cardID) {
|
|
3624
3889
|
return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
|
|
@@ -3663,6 +3928,7 @@ var init_core = __esm({
|
|
|
3663
3928
|
init_interfaces();
|
|
3664
3929
|
init_types_legacy();
|
|
3665
3930
|
init_user();
|
|
3931
|
+
init_strategyState();
|
|
3666
3932
|
init_Loggable();
|
|
3667
3933
|
init_util();
|
|
3668
3934
|
init_navigators();
|
|
@@ -3850,7 +4116,9 @@ var init_user_course_relDB = __esm({
|
|
|
3850
4116
|
function accomodateGuest() {
|
|
3851
4117
|
logger.log("[funnel] accomodateGuest() called");
|
|
3852
4118
|
if (typeof localStorage === "undefined") {
|
|
3853
|
-
logger.log(
|
|
4119
|
+
logger.log(
|
|
4120
|
+
"[funnel] localStorage not available (Node.js environment), returning default guest"
|
|
4121
|
+
);
|
|
3854
4122
|
return {
|
|
3855
4123
|
username: GuestUsername + "nodejs-test",
|
|
3856
4124
|
firstVisit: true
|
|
@@ -4830,6 +5098,55 @@ Currently logged-in as ${this._username}.`
|
|
|
4830
5098
|
async updateUserElo(courseId, elo) {
|
|
4831
5099
|
return updateUserElo(this._username, courseId, elo);
|
|
4832
5100
|
}
|
|
5101
|
+
async getStrategyState(courseId, strategyKey) {
|
|
5102
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
5103
|
+
try {
|
|
5104
|
+
const doc = await this.localDB.get(docId);
|
|
5105
|
+
return doc.data;
|
|
5106
|
+
} catch (e) {
|
|
5107
|
+
const err = e;
|
|
5108
|
+
if (err.status === 404) {
|
|
5109
|
+
return null;
|
|
5110
|
+
}
|
|
5111
|
+
throw e;
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
async putStrategyState(courseId, strategyKey, data) {
|
|
5115
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
5116
|
+
let existingRev;
|
|
5117
|
+
try {
|
|
5118
|
+
const existing = await this.localDB.get(docId);
|
|
5119
|
+
existingRev = existing._rev;
|
|
5120
|
+
} catch (e) {
|
|
5121
|
+
const err = e;
|
|
5122
|
+
if (err.status !== 404) {
|
|
5123
|
+
throw e;
|
|
5124
|
+
}
|
|
5125
|
+
}
|
|
5126
|
+
const doc = {
|
|
5127
|
+
_id: docId,
|
|
5128
|
+
_rev: existingRev,
|
|
5129
|
+
docType: "STRATEGY_STATE" /* STRATEGY_STATE */,
|
|
5130
|
+
courseId,
|
|
5131
|
+
strategyKey,
|
|
5132
|
+
data,
|
|
5133
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
5134
|
+
};
|
|
5135
|
+
await this.localDB.put(doc);
|
|
5136
|
+
}
|
|
5137
|
+
async deleteStrategyState(courseId, strategyKey) {
|
|
5138
|
+
const docId = buildStrategyStateId(courseId, strategyKey);
|
|
5139
|
+
try {
|
|
5140
|
+
const doc = await this.localDB.get(docId);
|
|
5141
|
+
await this.localDB.remove(doc);
|
|
5142
|
+
} catch (e) {
|
|
5143
|
+
const err = e;
|
|
5144
|
+
if (err.status === 404) {
|
|
5145
|
+
return;
|
|
5146
|
+
}
|
|
5147
|
+
throw e;
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
4833
5150
|
};
|
|
4834
5151
|
userCoursesDoc = "CourseRegistrations";
|
|
4835
5152
|
userClassroomsDoc = "ClassroomRegistrations";
|
|
@@ -4930,8 +5247,7 @@ var init_adminDB2 = __esm({
|
|
|
4930
5247
|
}
|
|
4931
5248
|
}
|
|
4932
5249
|
}
|
|
4933
|
-
|
|
4934
|
-
return dbs.map((db) => {
|
|
5250
|
+
return promisedCRDbs.map((db) => {
|
|
4935
5251
|
return {
|
|
4936
5252
|
...db.getConfig(),
|
|
4937
5253
|
_id: db._id
|