@vue-skuilder/db 0.1.32-a → 0.1.32-c
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/core/index.d.cts +16 -12
- package/dist/core/index.d.ts +16 -12
- package/dist/core/index.js +2279 -227
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +2256 -200
- package/dist/core/index.mjs.map +1 -1
- package/dist/{contentSource-Bdwkvqa8.d.ts → dataLayerProvider-BAn-LRh5.d.ts} +626 -83
- package/dist/{contentSource-DF1nUbPQ.d.cts → dataLayerProvider-BJqBlMIl.d.cts} +626 -83
- package/dist/impl/couch/index.d.cts +18 -3
- package/dist/impl/couch/index.d.ts +18 -3
- package/dist/impl/couch/index.js +2323 -224
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2311 -208
- 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 +2283 -231
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +2268 -212
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-BWvO-_rJ.d.ts → index-X6wHrURm.d.ts} +1 -1
- package/dist/{index-Ba7hYbHj.d.cts → index-m8MMGxxR.d.cts} +1 -1
- package/dist/index.d.cts +9 -381
- package/dist/index.d.ts +9 -381
- package/dist/index.js +9626 -8815
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9559 -8748
- package/dist/index.mjs.map +1 -1
- package/dist/{types-CJrLM1Ew.d.ts → types-DZ5dUqbL.d.ts} +1 -1
- package/dist/{types-W8n-B6HG.d.cts → types-ZL8tOPQZ.d.cts} +1 -1
- package/dist/{types-legacy-JXDxinpU.d.cts → types-legacy-C7r0T4OV.d.cts} +1 -1
- package/dist/{types-legacy-JXDxinpU.d.ts → types-legacy-C7r0T4OV.d.ts} +1 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/docs/navigators-architecture.md +2 -2
- package/package.json +2 -2
- package/src/core/interfaces/contentSource.ts +2 -1
- package/src/core/navigators/Pipeline.ts +51 -25
- package/src/core/navigators/PipelineDebugger.ts +49 -1
- package/src/core/navigators/filters/hierarchyDefinition.ts +92 -5
- package/src/core/navigators/filters/relativePriority.ts +7 -1
- package/src/core/navigators/generators/prescribed.ts +618 -43
- package/src/core/navigators/index.ts +2 -1
- package/src/impl/couch/CourseSyncService.ts +72 -4
- package/src/impl/couch/courseDB.ts +11 -0
- package/src/impl/static/courseDB.ts +13 -0
- package/src/study/SessionController.ts +276 -24
- package/src/study/services/EloService.ts +22 -3
- package/src/study/services/ResponseProcessor.ts +7 -3
- package/dist/dataLayerProvider-BKmVoyJR.d.ts +0 -67
- package/dist/dataLayerProvider-BQdfJuBN.d.cts +0 -67
|
@@ -553,13 +553,20 @@ function captureRun(report) {
|
|
|
553
553
|
runHistory.pop();
|
|
554
554
|
}
|
|
555
555
|
}
|
|
556
|
-
function
|
|
556
|
+
function parseCardElo(provenance) {
|
|
557
|
+
const eloEntry = provenance.find((p) => p.strategy === "elo");
|
|
558
|
+
if (!eloEntry?.reason) return void 0;
|
|
559
|
+
const match = eloEntry.reason.match(/card:\s*(\d+)/);
|
|
560
|
+
return match ? parseInt(match[1], 10) : void 0;
|
|
561
|
+
}
|
|
562
|
+
function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
|
|
557
563
|
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
558
564
|
const cards = allCards.map((card) => ({
|
|
559
565
|
cardId: card.cardId,
|
|
560
566
|
courseId: card.courseId,
|
|
561
567
|
origin: getOrigin(card),
|
|
562
568
|
finalScore: card.score,
|
|
569
|
+
cardElo: parseCardElo(card.provenance),
|
|
563
570
|
provenance: card.provenance,
|
|
564
571
|
tags: card.tags,
|
|
565
572
|
selected: selectedIds.has(card.cardId)
|
|
@@ -569,6 +576,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
569
576
|
return {
|
|
570
577
|
courseId,
|
|
571
578
|
courseName,
|
|
579
|
+
userElo,
|
|
572
580
|
generatorName,
|
|
573
581
|
generators,
|
|
574
582
|
generatedCount,
|
|
@@ -589,6 +597,7 @@ function printRunSummary(run) {
|
|
|
589
597
|
console.group(`\u{1F50D} Pipeline Run: ${run.courseId} (${run.courseName || "unnamed"})`);
|
|
590
598
|
logger.info(`Run ID: ${run.runId}`);
|
|
591
599
|
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
600
|
+
logger.info(`User ELO: ${run.userElo ?? "unknown"}`);
|
|
592
601
|
logger.info(`Generator: ${run.generatorName} \u2192 ${run.generatedCount} candidates`);
|
|
593
602
|
if (run.generators && run.generators.length > 0) {
|
|
594
603
|
console.group("Generator breakdown:");
|
|
@@ -675,8 +684,12 @@ var init_PipelineDebugger = __esm({
|
|
|
675
684
|
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
676
685
|
logger.info(`Course: ${card.courseId}`);
|
|
677
686
|
logger.info(`Origin: ${card.origin}`);
|
|
687
|
+
logger.info(`Card ELO: ${card.cardElo ?? "unknown"} | User ELO: ${run.userElo ?? "unknown"}`);
|
|
678
688
|
logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
|
|
679
689
|
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
690
|
+
if (card.tags && card.tags.length > 0) {
|
|
691
|
+
logger.info(`Tags (${card.tags.length}): ${card.tags.join(", ")}`);
|
|
692
|
+
}
|
|
680
693
|
logger.info("Provenance:");
|
|
681
694
|
logger.info(formatProvenance(card.provenance));
|
|
682
695
|
console.groupEnd();
|
|
@@ -840,6 +853,27 @@ var init_PipelineDebugger = __esm({
|
|
|
840
853
|
}
|
|
841
854
|
return _activePipeline.diagnoseCardSpace({ threshold });
|
|
842
855
|
},
|
|
856
|
+
/**
|
|
857
|
+
* Show user's per-tag ELO data. Useful for diagnosing hierarchy gate status.
|
|
858
|
+
*
|
|
859
|
+
* @param tagFilter - Optional glob pattern(s) to filter tags.
|
|
860
|
+
* Examples: 'gpc:expose:*', 'gpc:intro:t-T', ['gpc:expose:t-*', 'gpc:intro:t-*']
|
|
861
|
+
*/
|
|
862
|
+
async showTagElo(tagFilter) {
|
|
863
|
+
if (!_activePipeline) {
|
|
864
|
+
logger.info("[Pipeline Debug] No active pipeline. Run a session first.");
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
const status = await _activePipeline.getTagEloStatus(tagFilter);
|
|
868
|
+
const entries = Object.entries(status).sort(([a], [b]) => a.localeCompare(b));
|
|
869
|
+
if (entries.length === 0) {
|
|
870
|
+
logger.info(`[Pipeline Debug] No tag ELO data found${tagFilter ? ` for pattern: ${tagFilter}` : ""}.`);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
console.table(
|
|
874
|
+
Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
|
|
875
|
+
);
|
|
876
|
+
},
|
|
843
877
|
/**
|
|
844
878
|
* Show help.
|
|
845
879
|
*/
|
|
@@ -851,6 +885,7 @@ Commands:
|
|
|
851
885
|
.showLastRun() Show summary of most recent pipeline run
|
|
852
886
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
853
887
|
.showCard(cardId) Show provenance trail for a specific card
|
|
888
|
+
.showTagElo(pattern) Show user's tag ELO data (async). E.g. 'gpc:expose:*'
|
|
854
889
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
855
890
|
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
856
891
|
.showRegistry() Show navigator registry (classes + roles)
|
|
@@ -1164,60 +1199,423 @@ var prescribed_exports = {};
|
|
|
1164
1199
|
__export(prescribed_exports, {
|
|
1165
1200
|
default: () => PrescribedCardsGenerator
|
|
1166
1201
|
});
|
|
1167
|
-
|
|
1202
|
+
function dedupe(arr) {
|
|
1203
|
+
return [...new Set(arr)];
|
|
1204
|
+
}
|
|
1205
|
+
function isoNow() {
|
|
1206
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1207
|
+
}
|
|
1208
|
+
function clamp(value, min, max) {
|
|
1209
|
+
return Math.max(min, Math.min(max, value));
|
|
1210
|
+
}
|
|
1211
|
+
function matchesTagPattern(tag, pattern) {
|
|
1212
|
+
if (pattern === "*") return true;
|
|
1213
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1214
|
+
const re = new RegExp(`^${escaped}$`);
|
|
1215
|
+
return re.test(tag);
|
|
1216
|
+
}
|
|
1217
|
+
function pickTopByScore(cards, limit) {
|
|
1218
|
+
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1219
|
+
}
|
|
1220
|
+
var DEFAULT_FRESHNESS_WINDOW, DEFAULT_MAX_DIRECT_PER_RUN, DEFAULT_MAX_SUPPORT_PER_RUN, DEFAULT_HIERARCHY_DEPTH, DEFAULT_MIN_COUNT, BASE_TARGET_SCORE, BASE_SUPPORT_SCORE, MAX_TARGET_MULTIPLIER, MAX_SUPPORT_MULTIPLIER, LOCKED_TAG_PREFIXES, LESSON_GATE_PENALTY_TAG_HINT, PrescribedCardsGenerator;
|
|
1168
1221
|
var init_prescribed = __esm({
|
|
1169
1222
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1170
1223
|
"use strict";
|
|
1171
1224
|
init_navigators();
|
|
1172
1225
|
init_logger();
|
|
1226
|
+
DEFAULT_FRESHNESS_WINDOW = 3;
|
|
1227
|
+
DEFAULT_MAX_DIRECT_PER_RUN = 3;
|
|
1228
|
+
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1229
|
+
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1230
|
+
DEFAULT_MIN_COUNT = 3;
|
|
1231
|
+
BASE_TARGET_SCORE = 1;
|
|
1232
|
+
BASE_SUPPORT_SCORE = 0.8;
|
|
1233
|
+
MAX_TARGET_MULTIPLIER = 8;
|
|
1234
|
+
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1235
|
+
LOCKED_TAG_PREFIXES = ["concept:"];
|
|
1236
|
+
LESSON_GATE_PENALTY_TAG_HINT = "concept:";
|
|
1173
1237
|
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1174
1238
|
name;
|
|
1175
1239
|
config;
|
|
1176
1240
|
constructor(user, course, strategyData) {
|
|
1177
1241
|
super(user, course, strategyData);
|
|
1178
1242
|
this.name = strategyData.name || "Prescribed Cards";
|
|
1179
|
-
|
|
1180
|
-
const parsed = JSON.parse(strategyData.serializedData);
|
|
1181
|
-
this.config = { cardIds: parsed.cardIds || [] };
|
|
1182
|
-
} catch {
|
|
1183
|
-
this.config = { cardIds: [] };
|
|
1184
|
-
}
|
|
1243
|
+
this.config = this.parseConfig(strategyData.serializedData);
|
|
1185
1244
|
logger.debug(
|
|
1186
|
-
`[Prescribed] Initialized with ${this.config.
|
|
1245
|
+
`[Prescribed] Initialized with ${this.config.groups.length} groups and ${this.config.groups.reduce((n, g) => n + g.targetCardIds.length, 0)} targets`
|
|
1187
1246
|
);
|
|
1188
1247
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1248
|
+
get strategyKey() {
|
|
1249
|
+
return "PrescribedProgress";
|
|
1250
|
+
}
|
|
1251
|
+
async getWeightedCards(limit, context) {
|
|
1252
|
+
if (this.config.groups.length === 0 || limit <= 0) {
|
|
1191
1253
|
return [];
|
|
1192
1254
|
}
|
|
1193
1255
|
const courseId = this.course.getCourseID();
|
|
1194
1256
|
const activeCards = await this.user.getActiveCards();
|
|
1195
1257
|
const activeIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
1196
|
-
const
|
|
1197
|
-
|
|
1198
|
-
|
|
1258
|
+
const seenCards = await this.user.getSeenCards(courseId).catch(() => []);
|
|
1259
|
+
const seenIds = new Set(seenCards);
|
|
1260
|
+
const progress = await this.getStrategyState() ?? {
|
|
1261
|
+
updatedAt: isoNow(),
|
|
1262
|
+
groups: {}
|
|
1263
|
+
};
|
|
1264
|
+
const hierarchyConfigs = await this.loadHierarchyConfigs();
|
|
1265
|
+
const courseReg = await this.user.getCourseRegDoc(courseId).catch(() => null);
|
|
1266
|
+
const userGlobalElo = typeof courseReg?.elo === "number" ? courseReg.elo : courseReg?.elo?.global?.score ?? context?.userElo ?? 1e3;
|
|
1267
|
+
const userTagElo = typeof courseReg?.elo === "number" ? {} : courseReg?.elo?.tags ?? {};
|
|
1268
|
+
const allTargetIds = dedupe(this.config.groups.flatMap((g) => g.targetCardIds));
|
|
1269
|
+
const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
|
|
1270
|
+
const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
|
|
1271
|
+
const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
|
|
1272
|
+
const nextState = {
|
|
1273
|
+
updatedAt: isoNow(),
|
|
1274
|
+
groups: {}
|
|
1275
|
+
};
|
|
1276
|
+
const emitted = [];
|
|
1277
|
+
const emittedIds = /* @__PURE__ */ new Set();
|
|
1278
|
+
for (const group of this.config.groups) {
|
|
1279
|
+
const runtime = this.buildGroupRuntimeState({
|
|
1280
|
+
group,
|
|
1281
|
+
priorState: progress.groups[group.id],
|
|
1282
|
+
activeIds,
|
|
1283
|
+
seenIds,
|
|
1284
|
+
tagsByCard,
|
|
1285
|
+
hierarchyConfigs,
|
|
1286
|
+
userTagElo,
|
|
1287
|
+
userGlobalElo
|
|
1288
|
+
});
|
|
1289
|
+
nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
|
|
1290
|
+
const directCards = this.buildDirectTargetCards(
|
|
1291
|
+
runtime,
|
|
1292
|
+
courseId,
|
|
1293
|
+
emittedIds
|
|
1294
|
+
);
|
|
1295
|
+
const supportCards = this.buildSupportCards(
|
|
1296
|
+
runtime,
|
|
1297
|
+
courseId,
|
|
1298
|
+
emittedIds
|
|
1299
|
+
);
|
|
1300
|
+
emitted.push(...directCards, ...supportCards);
|
|
1301
|
+
}
|
|
1302
|
+
if (emitted.length === 0) {
|
|
1303
|
+
logger.debug("[Prescribed] No prescribed targets/support emitted this run");
|
|
1304
|
+
await this.putStrategyState(nextState).catch((e) => {
|
|
1305
|
+
logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
|
|
1306
|
+
});
|
|
1199
1307
|
return [];
|
|
1200
1308
|
}
|
|
1201
|
-
const
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1309
|
+
const finalCards = pickTopByScore(emitted, limit);
|
|
1310
|
+
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
1311
|
+
for (const card of finalCards) {
|
|
1312
|
+
const prov = card.provenance[0];
|
|
1313
|
+
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
1314
|
+
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
1315
|
+
if (!groupId) continue;
|
|
1316
|
+
if (!surfacedByGroup.has(groupId)) {
|
|
1317
|
+
surfacedByGroup.set(groupId, { targetIds: [], supportIds: [] });
|
|
1318
|
+
}
|
|
1319
|
+
surfacedByGroup.get(groupId)[mode].push(card.cardId);
|
|
1320
|
+
}
|
|
1321
|
+
for (const group of this.config.groups) {
|
|
1322
|
+
const groupState = nextState.groups[group.id];
|
|
1323
|
+
const surfaced = surfacedByGroup.get(group.id);
|
|
1324
|
+
if (surfaced && (surfaced.targetIds.length > 0 || surfaced.supportIds.length > 0)) {
|
|
1325
|
+
groupState.lastSurfacedAt = isoNow();
|
|
1326
|
+
groupState.sessionsSinceSurfaced = 0;
|
|
1327
|
+
if (surfaced.supportIds.length > 0) {
|
|
1328
|
+
groupState.lastSupportAt = isoNow();
|
|
1213
1329
|
}
|
|
1214
|
-
|
|
1215
|
-
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
await this.putStrategyState(nextState).catch((e) => {
|
|
1333
|
+
logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
|
|
1334
|
+
});
|
|
1216
1335
|
logger.info(
|
|
1217
|
-
`[Prescribed] Emitting ${
|
|
1336
|
+
`[Prescribed] Emitting ${finalCards.length} cards (${finalCards.filter((c) => c.provenance[0]?.reason.includes("mode=target")).length} target, ${finalCards.filter((c) => c.provenance[0]?.reason.includes("mode=support")).length} support)`
|
|
1218
1337
|
);
|
|
1338
|
+
return finalCards;
|
|
1339
|
+
}
|
|
1340
|
+
parseConfig(serializedData) {
|
|
1341
|
+
try {
|
|
1342
|
+
const parsed = JSON.parse(serializedData);
|
|
1343
|
+
const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
|
|
1344
|
+
const groups = groupsRaw.map((raw, i) => ({
|
|
1345
|
+
id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
|
|
1346
|
+
targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
|
|
1347
|
+
supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
|
|
1348
|
+
supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
|
|
1349
|
+
freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
|
|
1350
|
+
maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
|
|
1351
|
+
maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
|
|
1352
|
+
hierarchyWalk: {
|
|
1353
|
+
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
1354
|
+
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
1355
|
+
},
|
|
1356
|
+
retireOnEncounter: raw.retireOnEncounter !== false
|
|
1357
|
+
})).filter((g) => g.targetCardIds.length > 0);
|
|
1358
|
+
return { groups };
|
|
1359
|
+
} catch {
|
|
1360
|
+
return { groups: [] };
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
async loadHierarchyConfigs() {
|
|
1364
|
+
try {
|
|
1365
|
+
const strategies = await this.course.getNavigationStrategies();
|
|
1366
|
+
return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
|
|
1367
|
+
try {
|
|
1368
|
+
const parsed = JSON.parse(s.serializedData);
|
|
1369
|
+
return {
|
|
1370
|
+
prerequisites: parsed.prerequisites || {}
|
|
1371
|
+
};
|
|
1372
|
+
} catch {
|
|
1373
|
+
return { prerequisites: {} };
|
|
1374
|
+
}
|
|
1375
|
+
});
|
|
1376
|
+
} catch (e) {
|
|
1377
|
+
logger.debug(`[Prescribed] Failed to load hierarchy configs: ${e}`);
|
|
1378
|
+
return [];
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
buildGroupRuntimeState(args) {
|
|
1382
|
+
const {
|
|
1383
|
+
group,
|
|
1384
|
+
priorState,
|
|
1385
|
+
activeIds,
|
|
1386
|
+
seenIds,
|
|
1387
|
+
tagsByCard,
|
|
1388
|
+
hierarchyConfigs,
|
|
1389
|
+
userTagElo,
|
|
1390
|
+
userGlobalElo
|
|
1391
|
+
} = args;
|
|
1392
|
+
const encounteredTargets = /* @__PURE__ */ new Set();
|
|
1393
|
+
for (const cardId of group.targetCardIds) {
|
|
1394
|
+
if (activeIds.has(cardId) || seenIds.has(cardId)) {
|
|
1395
|
+
encounteredTargets.add(cardId);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
if (priorState?.encounteredCardIds?.length) {
|
|
1399
|
+
for (const cardId of priorState.encounteredCardIds) {
|
|
1400
|
+
encounteredTargets.add(cardId);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
const pendingTargets = group.targetCardIds.filter((id) => !encounteredTargets.has(id));
|
|
1404
|
+
const targetTags = /* @__PURE__ */ new Map();
|
|
1405
|
+
for (const cardId of pendingTargets) {
|
|
1406
|
+
targetTags.set(cardId, tagsByCard.get(cardId) ?? []);
|
|
1407
|
+
}
|
|
1408
|
+
const blockedTargets = [];
|
|
1409
|
+
const surfaceableTargets = [];
|
|
1410
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1411
|
+
for (const cardId of pendingTargets) {
|
|
1412
|
+
const tags = targetTags.get(cardId) ?? [];
|
|
1413
|
+
const resolution = this.resolveBlockedSupportTags(
|
|
1414
|
+
tags,
|
|
1415
|
+
hierarchyConfigs,
|
|
1416
|
+
userTagElo,
|
|
1417
|
+
userGlobalElo,
|
|
1418
|
+
group.hierarchyWalk?.enabled !== false,
|
|
1419
|
+
group.hierarchyWalk?.maxDepth ?? DEFAULT_HIERARCHY_DEPTH
|
|
1420
|
+
);
|
|
1421
|
+
if (resolution.blocked) {
|
|
1422
|
+
blockedTargets.push(cardId);
|
|
1423
|
+
resolution.supportTags.forEach((t) => supportTags.add(t));
|
|
1424
|
+
} else {
|
|
1425
|
+
surfaceableTargets.push(cardId);
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
const supportCandidates = dedupe([
|
|
1429
|
+
...group.supportCardIds ?? [],
|
|
1430
|
+
...this.findSupportCardsByTags(
|
|
1431
|
+
group,
|
|
1432
|
+
tagsByCard,
|
|
1433
|
+
[...supportTags]
|
|
1434
|
+
)
|
|
1435
|
+
]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
|
|
1436
|
+
const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
|
|
1437
|
+
const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
1438
|
+
const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
|
|
1439
|
+
const pressureMultiplier = pendingTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.75 + Math.min(2, pendingTargets.length * 0.1), 1, MAX_TARGET_MULTIPLIER);
|
|
1440
|
+
const supportMultiplier = blockedTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.5 + Math.min(1.5, blockedTargets.length * 0.15), 1, MAX_SUPPORT_MULTIPLIER);
|
|
1441
|
+
return {
|
|
1442
|
+
group,
|
|
1443
|
+
encounteredTargets,
|
|
1444
|
+
pendingTargets,
|
|
1445
|
+
blockedTargets,
|
|
1446
|
+
surfaceableTargets,
|
|
1447
|
+
targetTags,
|
|
1448
|
+
supportCandidates,
|
|
1449
|
+
supportTags: [...supportTags],
|
|
1450
|
+
pressureMultiplier,
|
|
1451
|
+
supportMultiplier
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
buildNextGroupState(runtime, prior) {
|
|
1455
|
+
const carriedSessions = prior?.sessionsSinceSurfaced ?? 0;
|
|
1456
|
+
const surfacedThisRun = false;
|
|
1457
|
+
return {
|
|
1458
|
+
encounteredCardIds: [...runtime.encounteredTargets].sort(),
|
|
1459
|
+
lastSurfacedAt: prior?.lastSurfacedAt ?? null,
|
|
1460
|
+
sessionsSinceSurfaced: surfacedThisRun ? 0 : carriedSessions + 1,
|
|
1461
|
+
lastSupportAt: prior?.lastSupportAt ?? null,
|
|
1462
|
+
blockedTargetIds: [...runtime.blockedTargets].sort(),
|
|
1463
|
+
lastResolvedSupportTags: [...runtime.supportTags].sort()
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
buildDirectTargetCards(runtime, courseId, emittedIds) {
|
|
1467
|
+
const maxDirect = runtime.group.maxDirectTargetsPerRun ?? DEFAULT_MAX_DIRECT_PER_RUN;
|
|
1468
|
+
const directIds = runtime.surfaceableTargets.filter((id) => !emittedIds.has(id)).slice(0, maxDirect);
|
|
1469
|
+
const cards = [];
|
|
1470
|
+
for (const cardId of directIds) {
|
|
1471
|
+
emittedIds.add(cardId);
|
|
1472
|
+
cards.push({
|
|
1473
|
+
cardId,
|
|
1474
|
+
courseId,
|
|
1475
|
+
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1476
|
+
provenance: [
|
|
1477
|
+
{
|
|
1478
|
+
strategy: "prescribed",
|
|
1479
|
+
strategyName: this.strategyName || this.name,
|
|
1480
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1481
|
+
action: "generated",
|
|
1482
|
+
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1483
|
+
reason: `mode=target;group=${runtime.group.id};pending=${runtime.pendingTargets.length};surfaceable=${runtime.surfaceableTargets.length};blocked=${runtime.blockedTargets.length};multiplier=${runtime.pressureMultiplier.toFixed(2)}`
|
|
1484
|
+
}
|
|
1485
|
+
]
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
return cards;
|
|
1489
|
+
}
|
|
1490
|
+
buildSupportCards(runtime, courseId, emittedIds) {
|
|
1491
|
+
if (runtime.blockedTargets.length === 0 || runtime.supportCandidates.length === 0) {
|
|
1492
|
+
return [];
|
|
1493
|
+
}
|
|
1494
|
+
const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
|
|
1495
|
+
const supportIds = runtime.supportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
|
|
1496
|
+
const cards = [];
|
|
1497
|
+
for (const cardId of supportIds) {
|
|
1498
|
+
emittedIds.add(cardId);
|
|
1499
|
+
cards.push({
|
|
1500
|
+
cardId,
|
|
1501
|
+
courseId,
|
|
1502
|
+
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1503
|
+
provenance: [
|
|
1504
|
+
{
|
|
1505
|
+
strategy: "prescribed",
|
|
1506
|
+
strategyName: this.strategyName || this.name,
|
|
1507
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1508
|
+
action: "generated",
|
|
1509
|
+
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1510
|
+
reason: `mode=support;group=${runtime.group.id};blocked=${runtime.blockedTargets.length};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.supportMultiplier.toFixed(2)}`
|
|
1511
|
+
}
|
|
1512
|
+
]
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1219
1515
|
return cards;
|
|
1220
1516
|
}
|
|
1517
|
+
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
1518
|
+
if (supportTags.length === 0) {
|
|
1519
|
+
return [];
|
|
1520
|
+
}
|
|
1521
|
+
const explicitSupportIds = group.supportCardIds ?? [];
|
|
1522
|
+
const explicitPatterns = group.supportTagPatterns ?? [];
|
|
1523
|
+
if (explicitSupportIds.length === 0 && explicitPatterns.length === 0) {
|
|
1524
|
+
return [];
|
|
1525
|
+
}
|
|
1526
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1527
|
+
for (const cardId of explicitSupportIds) {
|
|
1528
|
+
const cardTags = tagsByCard.get(cardId) ?? [];
|
|
1529
|
+
const matchesResolved = supportTags.some((supportTag) => cardTags.includes(supportTag));
|
|
1530
|
+
const matchesPattern = explicitPatterns.some(
|
|
1531
|
+
(pattern) => cardTags.some((tag) => matchesTagPattern(tag, pattern))
|
|
1532
|
+
);
|
|
1533
|
+
if (matchesResolved || matchesPattern) {
|
|
1534
|
+
candidates.add(cardId);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
return [...candidates];
|
|
1538
|
+
}
|
|
1539
|
+
resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
|
|
1540
|
+
if (!hierarchyWalkEnabled || targetTags.length === 0 || hierarchyConfigs.length === 0) {
|
|
1541
|
+
return {
|
|
1542
|
+
blocked: false,
|
|
1543
|
+
supportTags: []
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1547
|
+
let blocked = false;
|
|
1548
|
+
for (const targetTag of targetTags) {
|
|
1549
|
+
for (const hierarchy of hierarchyConfigs) {
|
|
1550
|
+
const prereqs = hierarchy.prerequisites[targetTag];
|
|
1551
|
+
if (!prereqs || prereqs.length === 0) continue;
|
|
1552
|
+
const unmet = prereqs.filter(
|
|
1553
|
+
(pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo)
|
|
1554
|
+
);
|
|
1555
|
+
if (unmet.length === 0) {
|
|
1556
|
+
continue;
|
|
1557
|
+
}
|
|
1558
|
+
blocked = true;
|
|
1559
|
+
for (const prereq of unmet) {
|
|
1560
|
+
this.collectSupportTagsRecursive(
|
|
1561
|
+
prereq.tag,
|
|
1562
|
+
hierarchyConfigs,
|
|
1563
|
+
userTagElo,
|
|
1564
|
+
userGlobalElo,
|
|
1565
|
+
maxDepth,
|
|
1566
|
+
/* @__PURE__ */ new Set(),
|
|
1567
|
+
supportTags
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
return { blocked, supportTags: [...supportTags] };
|
|
1573
|
+
}
|
|
1574
|
+
collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
|
|
1575
|
+
if (depth < 0 || visited.has(tag)) return;
|
|
1576
|
+
if (this.isHardGatedTag(tag)) return;
|
|
1577
|
+
visited.add(tag);
|
|
1578
|
+
let walkedFurther = false;
|
|
1579
|
+
for (const hierarchy of hierarchyConfigs) {
|
|
1580
|
+
const prereqs = hierarchy.prerequisites[tag];
|
|
1581
|
+
if (!prereqs || prereqs.length === 0) continue;
|
|
1582
|
+
const unmet = prereqs.filter(
|
|
1583
|
+
(pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo)
|
|
1584
|
+
);
|
|
1585
|
+
if (unmet.length > 0 && depth > 0) {
|
|
1586
|
+
walkedFurther = true;
|
|
1587
|
+
for (const prereq of unmet) {
|
|
1588
|
+
this.collectSupportTagsRecursive(
|
|
1589
|
+
prereq.tag,
|
|
1590
|
+
hierarchyConfigs,
|
|
1591
|
+
userTagElo,
|
|
1592
|
+
userGlobalElo,
|
|
1593
|
+
depth - 1,
|
|
1594
|
+
visited,
|
|
1595
|
+
out
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
if (!walkedFurther) {
|
|
1601
|
+
out.add(tag);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
isHardGatedTag(tag) {
|
|
1605
|
+
return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
|
|
1606
|
+
}
|
|
1607
|
+
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
1608
|
+
if (!userTagElo) return false;
|
|
1609
|
+
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
|
|
1610
|
+
if (userTagElo.count < minCount) return false;
|
|
1611
|
+
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1612
|
+
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
1613
|
+
}
|
|
1614
|
+
if (prereq.masteryThreshold?.minCount !== void 0) {
|
|
1615
|
+
return true;
|
|
1616
|
+
}
|
|
1617
|
+
return userTagElo.score >= userGlobalElo;
|
|
1618
|
+
}
|
|
1221
1619
|
};
|
|
1222
1620
|
}
|
|
1223
1621
|
});
|
|
@@ -1580,13 +1978,14 @@ var hierarchyDefinition_exports = {};
|
|
|
1580
1978
|
__export(hierarchyDefinition_exports, {
|
|
1581
1979
|
default: () => HierarchyDefinitionNavigator
|
|
1582
1980
|
});
|
|
1583
|
-
var import_common6,
|
|
1981
|
+
var import_common6, DEFAULT_MIN_COUNT2, HierarchyDefinitionNavigator;
|
|
1584
1982
|
var init_hierarchyDefinition = __esm({
|
|
1585
1983
|
"src/core/navigators/filters/hierarchyDefinition.ts"() {
|
|
1586
1984
|
"use strict";
|
|
1587
1985
|
init_navigators();
|
|
1588
1986
|
import_common6 = require("@vue-skuilder/common");
|
|
1589
|
-
|
|
1987
|
+
init_logger();
|
|
1988
|
+
DEFAULT_MIN_COUNT2 = 3;
|
|
1590
1989
|
HierarchyDefinitionNavigator = class extends ContentNavigator {
|
|
1591
1990
|
config;
|
|
1592
1991
|
/** Human-readable name for CardFilter interface */
|
|
@@ -1613,7 +2012,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1613
2012
|
*/
|
|
1614
2013
|
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
1615
2014
|
if (!userTagElo) return false;
|
|
1616
|
-
const minCount = prereq.masteryThreshold?.minCount ??
|
|
2015
|
+
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT2;
|
|
1617
2016
|
if (userTagElo.count < minCount) return false;
|
|
1618
2017
|
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1619
2018
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
@@ -1714,18 +2113,55 @@ var init_hierarchyDefinition = __esm({
|
|
|
1714
2113
|
}
|
|
1715
2114
|
return boosts;
|
|
1716
2115
|
}
|
|
2116
|
+
/**
|
|
2117
|
+
* Build a map of gated tag → max configured targetBoost for all *open* gates.
|
|
2118
|
+
*
|
|
2119
|
+
* When a gate opens (prereqs met), cards carrying the gated tag get boosted —
|
|
2120
|
+
* ensuring newly-unlocked content surfaces promptly. The boost is a static
|
|
2121
|
+
* multiplier; natural ELO/SRS deprioritization after interaction handles decay.
|
|
2122
|
+
*/
|
|
2123
|
+
getTargetBoosts(unlockedTags) {
|
|
2124
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
2125
|
+
const configKeys = Object.keys(this.config.prerequisites);
|
|
2126
|
+
const unlockedArr = [...unlockedTags];
|
|
2127
|
+
logger.info(
|
|
2128
|
+
`[HierarchyDefinition:targetBoost:trace] ${this.name} | configKeys=${configKeys.length}, unlocked=${unlockedArr.length} (${unlockedArr.slice(0, 5).join(", ")}${unlockedArr.length > 5 ? "..." : ""})`
|
|
2129
|
+
);
|
|
2130
|
+
for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
|
|
2131
|
+
if (!unlockedTags.has(tagId)) continue;
|
|
2132
|
+
logger.info(
|
|
2133
|
+
`[HierarchyDefinition:targetBoost:trace] UNLOCKED ${tagId}: ${prereqs.length} prereqs, raw=${JSON.stringify(prereqs.map((p) => ({ tag: p.tag, tb: p.targetBoost })))}`
|
|
2134
|
+
);
|
|
2135
|
+
for (const prereq of prereqs) {
|
|
2136
|
+
if (!prereq.targetBoost || prereq.targetBoost <= 1) continue;
|
|
2137
|
+
const existing = boosts.get(tagId) ?? 1;
|
|
2138
|
+
boosts.set(tagId, Math.max(existing, prereq.targetBoost));
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
if (boosts.size > 0) {
|
|
2142
|
+
logger.info(
|
|
2143
|
+
`[HierarchyDefinition] targetBoosts active: ${[...boosts.entries()].map(([t, b]) => `${t}=\xD7${b}`).join(", ")}`
|
|
2144
|
+
);
|
|
2145
|
+
} else {
|
|
2146
|
+
logger.info(
|
|
2147
|
+
`[HierarchyDefinition:targetBoost:trace] no targetBoosts found despite ${unlockedArr.length} unlocked tags`
|
|
2148
|
+
);
|
|
2149
|
+
}
|
|
2150
|
+
return boosts;
|
|
2151
|
+
}
|
|
1717
2152
|
/**
|
|
1718
2153
|
* CardFilter.transform implementation.
|
|
1719
2154
|
*
|
|
1720
|
-
*
|
|
1721
|
-
* 1. Cards with locked tags receive score * 0.
|
|
1722
|
-
* 2. Cards carrying prereq tags of closed gates receive
|
|
1723
|
-
*
|
|
2155
|
+
* Three effects:
|
|
2156
|
+
* 1. Cards with locked tags receive score * 0.02 (gating penalty)
|
|
2157
|
+
* 2. Cards carrying prereq tags of closed gates receive preReqBoost
|
|
2158
|
+
* 3. Cards carrying gated tags of open gates receive targetBoost
|
|
1724
2159
|
*/
|
|
1725
2160
|
async transform(cards, context) {
|
|
1726
2161
|
const masteredTags = await this.getMasteredTags(context);
|
|
1727
2162
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
1728
2163
|
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
2164
|
+
const targetBoosts = this.getTargetBoosts(unlockedTags);
|
|
1729
2165
|
const gated = [];
|
|
1730
2166
|
for (const card of cards) {
|
|
1731
2167
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
@@ -1753,6 +2189,29 @@ var init_hierarchyDefinition = __esm({
|
|
|
1753
2189
|
finalScore *= maxBoost;
|
|
1754
2190
|
action = "boosted";
|
|
1755
2191
|
finalReason = `${reason} | preReqBoost \xD7${maxBoost.toFixed(2)} for ${boostedPrereqs.join(", ")}`;
|
|
2192
|
+
logger.info(
|
|
2193
|
+
`[HierarchyDefinition] preReqBoost \xD7${maxBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedPrereqs.join(", ")}] (score: ${card.score.toFixed(3)} \u2192 ${finalScore.toFixed(3)})`
|
|
2194
|
+
);
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
if (isUnlocked && targetBoosts.size > 0) {
|
|
2198
|
+
const cardTags = card.tags ?? [];
|
|
2199
|
+
let maxTargetBoost = 1;
|
|
2200
|
+
const boostedTargets = [];
|
|
2201
|
+
for (const tag of cardTags) {
|
|
2202
|
+
const boost = targetBoosts.get(tag);
|
|
2203
|
+
if (boost && boost > maxTargetBoost) {
|
|
2204
|
+
maxTargetBoost = boost;
|
|
2205
|
+
boostedTargets.push(tag);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
if (maxTargetBoost > 1) {
|
|
2209
|
+
finalScore *= maxTargetBoost;
|
|
2210
|
+
action = "boosted";
|
|
2211
|
+
finalReason = `${finalReason} | targetBoost \xD7${maxTargetBoost.toFixed(2)} for ${boostedTargets.join(", ")}`;
|
|
2212
|
+
logger.info(
|
|
2213
|
+
`[HierarchyDefinition] targetBoost \xD7${maxTargetBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedTargets.join(", ")}] (score: ${card.score.toFixed(3)} \u2192 ${finalScore.toFixed(3)})`
|
|
2214
|
+
);
|
|
1756
2215
|
}
|
|
1757
2216
|
}
|
|
1758
2217
|
gated.push({
|
|
@@ -1939,13 +2398,13 @@ var interferenceMitigator_exports = {};
|
|
|
1939
2398
|
__export(interferenceMitigator_exports, {
|
|
1940
2399
|
default: () => InterferenceMitigatorNavigator
|
|
1941
2400
|
});
|
|
1942
|
-
var import_common7,
|
|
2401
|
+
var import_common7, DEFAULT_MIN_COUNT3, DEFAULT_MIN_ELAPSED_DAYS, DEFAULT_INTERFERENCE_DECAY, InterferenceMitigatorNavigator;
|
|
1943
2402
|
var init_interferenceMitigator = __esm({
|
|
1944
2403
|
"src/core/navigators/filters/interferenceMitigator.ts"() {
|
|
1945
2404
|
"use strict";
|
|
1946
2405
|
init_navigators();
|
|
1947
2406
|
import_common7 = require("@vue-skuilder/common");
|
|
1948
|
-
|
|
2407
|
+
DEFAULT_MIN_COUNT3 = 10;
|
|
1949
2408
|
DEFAULT_MIN_ELAPSED_DAYS = 3;
|
|
1950
2409
|
DEFAULT_INTERFERENCE_DECAY = 0.8;
|
|
1951
2410
|
InterferenceMitigatorNavigator = class extends ContentNavigator {
|
|
@@ -1970,7 +2429,7 @@ var init_interferenceMitigator = __esm({
|
|
|
1970
2429
|
return {
|
|
1971
2430
|
interferenceSets: sets,
|
|
1972
2431
|
maturityThreshold: {
|
|
1973
|
-
minCount: parsed.maturityThreshold?.minCount ??
|
|
2432
|
+
minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3,
|
|
1974
2433
|
minElo: parsed.maturityThreshold?.minElo,
|
|
1975
2434
|
minElapsedDays: parsed.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS
|
|
1976
2435
|
},
|
|
@@ -1980,7 +2439,7 @@ var init_interferenceMitigator = __esm({
|
|
|
1980
2439
|
return {
|
|
1981
2440
|
interferenceSets: [],
|
|
1982
2441
|
maturityThreshold: {
|
|
1983
|
-
minCount:
|
|
2442
|
+
minCount: DEFAULT_MIN_COUNT3,
|
|
1984
2443
|
minElapsedDays: DEFAULT_MIN_ELAPSED_DAYS
|
|
1985
2444
|
},
|
|
1986
2445
|
defaultDecay: DEFAULT_INTERFERENCE_DECAY
|
|
@@ -2027,7 +2486,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2027
2486
|
try {
|
|
2028
2487
|
const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
|
|
2029
2488
|
const userElo = (0, import_common7.toCourseElo)(courseReg.elo);
|
|
2030
|
-
const minCount = this.config.maturityThreshold?.minCount ??
|
|
2489
|
+
const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3;
|
|
2031
2490
|
const minElo = this.config.maturityThreshold?.minElo;
|
|
2032
2491
|
const minElapsedDays = this.config.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS;
|
|
2033
2492
|
const minCountForElapsed = minElapsedDays * 2;
|
|
@@ -2262,7 +2721,7 @@ var init_relativePriority = __esm({
|
|
|
2262
2721
|
const cardTags = card.tags ?? [];
|
|
2263
2722
|
const priority = this.computeCardPriority(cardTags);
|
|
2264
2723
|
const boostFactor = this.computeBoostFactor(priority);
|
|
2265
|
-
const finalScore = Math.max(0,
|
|
2724
|
+
const finalScore = Math.max(0, card.score * boostFactor);
|
|
2266
2725
|
const action = boostFactor > 1 ? "boosted" : boostFactor < 1 ? "penalized" : "passed";
|
|
2267
2726
|
const reason = this.buildPriorityReason(cardTags, priority, boostFactor, finalScore);
|
|
2268
2727
|
return {
|
|
@@ -2285,167 +2744,1721 @@ var init_relativePriority = __esm({
|
|
|
2285
2744
|
return adjusted;
|
|
2286
2745
|
}
|
|
2287
2746
|
/**
|
|
2288
|
-
* Legacy getWeightedCards - now throws as filters should not be used as generators.
|
|
2289
|
-
*
|
|
2290
|
-
* Use transform() via Pipeline instead.
|
|
2747
|
+
* Legacy getWeightedCards - now throws as filters should not be used as generators.
|
|
2748
|
+
*
|
|
2749
|
+
* Use transform() via Pipeline instead.
|
|
2750
|
+
*/
|
|
2751
|
+
async getWeightedCards(_limit) {
|
|
2752
|
+
throw new Error(
|
|
2753
|
+
"RelativePriorityNavigator is a filter and should not be used as a generator. Use Pipeline with a generator and this filter via transform()."
|
|
2754
|
+
);
|
|
2755
|
+
}
|
|
2756
|
+
};
|
|
2757
|
+
}
|
|
2758
|
+
});
|
|
2759
|
+
|
|
2760
|
+
// src/core/navigators/filters/types.ts
|
|
2761
|
+
var types_exports2 = {};
|
|
2762
|
+
var init_types2 = __esm({
|
|
2763
|
+
"src/core/navigators/filters/types.ts"() {
|
|
2764
|
+
"use strict";
|
|
2765
|
+
}
|
|
2766
|
+
});
|
|
2767
|
+
|
|
2768
|
+
// src/core/navigators/filters/userGoalStub.ts
|
|
2769
|
+
var userGoalStub_exports = {};
|
|
2770
|
+
__export(userGoalStub_exports, {
|
|
2771
|
+
USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
|
|
2772
|
+
});
|
|
2773
|
+
var USER_GOAL_NAVIGATOR_STUB;
|
|
2774
|
+
var init_userGoalStub = __esm({
|
|
2775
|
+
"src/core/navigators/filters/userGoalStub.ts"() {
|
|
2776
|
+
"use strict";
|
|
2777
|
+
USER_GOAL_NAVIGATOR_STUB = true;
|
|
2778
|
+
}
|
|
2779
|
+
});
|
|
2780
|
+
|
|
2781
|
+
// import("./filters/**/*") in src/core/navigators/index.ts
|
|
2782
|
+
var globImport_filters;
|
|
2783
|
+
var init_2 = __esm({
|
|
2784
|
+
'import("./filters/**/*") in src/core/navigators/index.ts'() {
|
|
2785
|
+
globImport_filters = __glob({
|
|
2786
|
+
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
2787
|
+
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
2788
|
+
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
2789
|
+
"./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
|
|
2790
|
+
"./filters/inferredPreferenceStub.ts": () => Promise.resolve().then(() => (init_inferredPreferenceStub(), inferredPreferenceStub_exports)),
|
|
2791
|
+
"./filters/interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
|
|
2792
|
+
"./filters/relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
|
|
2793
|
+
"./filters/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
|
|
2794
|
+
"./filters/userGoalStub.ts": () => Promise.resolve().then(() => (init_userGoalStub(), userGoalStub_exports)),
|
|
2795
|
+
"./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports))
|
|
2796
|
+
});
|
|
2797
|
+
}
|
|
2798
|
+
});
|
|
2799
|
+
|
|
2800
|
+
// src/core/orchestration/gradient.ts
|
|
2801
|
+
var init_gradient = __esm({
|
|
2802
|
+
"src/core/orchestration/gradient.ts"() {
|
|
2803
|
+
"use strict";
|
|
2804
|
+
init_logger();
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
|
|
2808
|
+
// src/core/orchestration/learning.ts
|
|
2809
|
+
var init_learning = __esm({
|
|
2810
|
+
"src/core/orchestration/learning.ts"() {
|
|
2811
|
+
"use strict";
|
|
2812
|
+
init_contentNavigationStrategy();
|
|
2813
|
+
init_types_legacy();
|
|
2814
|
+
init_logger();
|
|
2815
|
+
}
|
|
2816
|
+
});
|
|
2817
|
+
|
|
2818
|
+
// src/core/orchestration/signal.ts
|
|
2819
|
+
var init_signal = __esm({
|
|
2820
|
+
"src/core/orchestration/signal.ts"() {
|
|
2821
|
+
"use strict";
|
|
2822
|
+
}
|
|
2823
|
+
});
|
|
2824
|
+
|
|
2825
|
+
// src/core/orchestration/recording.ts
|
|
2826
|
+
var init_recording = __esm({
|
|
2827
|
+
"src/core/orchestration/recording.ts"() {
|
|
2828
|
+
"use strict";
|
|
2829
|
+
init_signal();
|
|
2830
|
+
init_types_legacy();
|
|
2831
|
+
init_logger();
|
|
2832
|
+
}
|
|
2833
|
+
});
|
|
2834
|
+
|
|
2835
|
+
// src/core/orchestration/index.ts
|
|
2836
|
+
function fnv1a(str) {
|
|
2837
|
+
let hash = 2166136261;
|
|
2838
|
+
for (let i = 0; i < str.length; i++) {
|
|
2839
|
+
hash ^= str.charCodeAt(i);
|
|
2840
|
+
hash = Math.imul(hash, 16777619);
|
|
2841
|
+
}
|
|
2842
|
+
return hash >>> 0;
|
|
2843
|
+
}
|
|
2844
|
+
function computeDeviation(userId, strategyId, salt) {
|
|
2845
|
+
const input = `${userId}:${strategyId}:${salt}`;
|
|
2846
|
+
const hash = fnv1a(input);
|
|
2847
|
+
const normalized = hash / 4294967296;
|
|
2848
|
+
return normalized * 2 - 1;
|
|
2849
|
+
}
|
|
2850
|
+
function computeSpread(confidence) {
|
|
2851
|
+
const clampedConfidence = Math.max(0, Math.min(1, confidence));
|
|
2852
|
+
return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
|
|
2853
|
+
}
|
|
2854
|
+
function computeEffectiveWeight(learnable, userId, strategyId, salt) {
|
|
2855
|
+
const deviation = computeDeviation(userId, strategyId, salt);
|
|
2856
|
+
const spread = computeSpread(learnable.confidence);
|
|
2857
|
+
const adjustment = deviation * spread * learnable.weight;
|
|
2858
|
+
const effective = learnable.weight + adjustment;
|
|
2859
|
+
return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
|
|
2860
|
+
}
|
|
2861
|
+
async function createOrchestrationContext(user, course) {
|
|
2862
|
+
let courseConfig;
|
|
2863
|
+
try {
|
|
2864
|
+
courseConfig = await course.getCourseConfig();
|
|
2865
|
+
} catch (e) {
|
|
2866
|
+
logger.error(`[Orchestration] Failed to load course config: ${e}`);
|
|
2867
|
+
courseConfig = {
|
|
2868
|
+
name: "Unknown",
|
|
2869
|
+
description: "",
|
|
2870
|
+
public: false,
|
|
2871
|
+
deleted: false,
|
|
2872
|
+
creator: "",
|
|
2873
|
+
admins: [],
|
|
2874
|
+
moderators: [],
|
|
2875
|
+
dataShapes: [],
|
|
2876
|
+
questionTypes: [],
|
|
2877
|
+
orchestration: { salt: "default" }
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
const userId = user.getUsername();
|
|
2881
|
+
const salt = courseConfig.orchestration?.salt || "default_salt";
|
|
2882
|
+
return {
|
|
2883
|
+
user,
|
|
2884
|
+
course,
|
|
2885
|
+
userId,
|
|
2886
|
+
courseConfig,
|
|
2887
|
+
getEffectiveWeight(strategyId, learnable) {
|
|
2888
|
+
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
2889
|
+
},
|
|
2890
|
+
getDeviation(strategyId) {
|
|
2891
|
+
return computeDeviation(userId, strategyId, salt);
|
|
2892
|
+
}
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
|
|
2896
|
+
var init_orchestration = __esm({
|
|
2897
|
+
"src/core/orchestration/index.ts"() {
|
|
2898
|
+
"use strict";
|
|
2899
|
+
init_logger();
|
|
2900
|
+
init_gradient();
|
|
2901
|
+
init_learning();
|
|
2902
|
+
init_signal();
|
|
2903
|
+
init_recording();
|
|
2904
|
+
MIN_SPREAD = 0.1;
|
|
2905
|
+
MAX_SPREAD = 0.5;
|
|
2906
|
+
MIN_WEIGHT = 0.1;
|
|
2907
|
+
MAX_WEIGHT = 3;
|
|
2908
|
+
}
|
|
2909
|
+
});
|
|
2910
|
+
|
|
2911
|
+
// src/study/SpacedRepetition.ts
|
|
2912
|
+
var import_moment4, import_common8, duration;
|
|
2913
|
+
var init_SpacedRepetition = __esm({
|
|
2914
|
+
"src/study/SpacedRepetition.ts"() {
|
|
2915
|
+
"use strict";
|
|
2916
|
+
init_util();
|
|
2917
|
+
import_moment4 = __toESM(require("moment"), 1);
|
|
2918
|
+
import_common8 = require("@vue-skuilder/common");
|
|
2919
|
+
init_logger();
|
|
2920
|
+
duration = import_moment4.default.duration;
|
|
2921
|
+
}
|
|
2922
|
+
});
|
|
2923
|
+
|
|
2924
|
+
// src/study/services/SrsService.ts
|
|
2925
|
+
var import_moment5;
|
|
2926
|
+
var init_SrsService = __esm({
|
|
2927
|
+
"src/study/services/SrsService.ts"() {
|
|
2928
|
+
"use strict";
|
|
2929
|
+
import_moment5 = __toESM(require("moment"), 1);
|
|
2930
|
+
init_couch();
|
|
2931
|
+
init_SpacedRepetition();
|
|
2932
|
+
init_logger();
|
|
2933
|
+
}
|
|
2934
|
+
});
|
|
2935
|
+
|
|
2936
|
+
// src/study/services/EloService.ts
|
|
2937
|
+
var import_common9;
|
|
2938
|
+
var init_EloService = __esm({
|
|
2939
|
+
"src/study/services/EloService.ts"() {
|
|
2940
|
+
"use strict";
|
|
2941
|
+
import_common9 = require("@vue-skuilder/common");
|
|
2942
|
+
init_logger();
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2945
|
+
|
|
2946
|
+
// src/study/services/ResponseProcessor.ts
|
|
2947
|
+
var import_common10;
|
|
2948
|
+
var init_ResponseProcessor = __esm({
|
|
2949
|
+
"src/study/services/ResponseProcessor.ts"() {
|
|
2950
|
+
"use strict";
|
|
2951
|
+
init_core();
|
|
2952
|
+
init_logger();
|
|
2953
|
+
import_common10 = require("@vue-skuilder/common");
|
|
2954
|
+
}
|
|
2955
|
+
});
|
|
2956
|
+
|
|
2957
|
+
// src/study/services/CardHydrationService.ts
|
|
2958
|
+
var import_common11;
|
|
2959
|
+
var init_CardHydrationService = __esm({
|
|
2960
|
+
"src/study/services/CardHydrationService.ts"() {
|
|
2961
|
+
"use strict";
|
|
2962
|
+
import_common11 = require("@vue-skuilder/common");
|
|
2963
|
+
init_logger();
|
|
2964
|
+
}
|
|
2965
|
+
});
|
|
2966
|
+
|
|
2967
|
+
// src/study/ItemQueue.ts
|
|
2968
|
+
var init_ItemQueue = __esm({
|
|
2969
|
+
"src/study/ItemQueue.ts"() {
|
|
2970
|
+
"use strict";
|
|
2971
|
+
}
|
|
2972
|
+
});
|
|
2973
|
+
|
|
2974
|
+
// src/util/packer/types.ts
|
|
2975
|
+
var init_types3 = __esm({
|
|
2976
|
+
"src/util/packer/types.ts"() {
|
|
2977
|
+
"use strict";
|
|
2978
|
+
}
|
|
2979
|
+
});
|
|
2980
|
+
|
|
2981
|
+
// src/util/packer/CouchDBToStaticPacker.ts
|
|
2982
|
+
var init_CouchDBToStaticPacker = __esm({
|
|
2983
|
+
"src/util/packer/CouchDBToStaticPacker.ts"() {
|
|
2984
|
+
"use strict";
|
|
2985
|
+
init_types_legacy();
|
|
2986
|
+
init_logger();
|
|
2987
|
+
}
|
|
2988
|
+
});
|
|
2989
|
+
|
|
2990
|
+
// src/util/packer/index.ts
|
|
2991
|
+
var init_packer = __esm({
|
|
2992
|
+
"src/util/packer/index.ts"() {
|
|
2993
|
+
"use strict";
|
|
2994
|
+
init_types3();
|
|
2995
|
+
init_CouchDBToStaticPacker();
|
|
2996
|
+
}
|
|
2997
|
+
});
|
|
2998
|
+
|
|
2999
|
+
// src/util/migrator/types.ts
|
|
3000
|
+
var DEFAULT_MIGRATION_OPTIONS;
|
|
3001
|
+
var init_types4 = __esm({
|
|
3002
|
+
"src/util/migrator/types.ts"() {
|
|
3003
|
+
"use strict";
|
|
3004
|
+
DEFAULT_MIGRATION_OPTIONS = {
|
|
3005
|
+
chunkBatchSize: 100,
|
|
3006
|
+
validateRoundTrip: false,
|
|
3007
|
+
cleanupOnFailure: true,
|
|
3008
|
+
timeout: 3e5
|
|
3009
|
+
// 5 minutes
|
|
3010
|
+
};
|
|
3011
|
+
}
|
|
3012
|
+
});
|
|
3013
|
+
|
|
3014
|
+
// src/util/migrator/FileSystemAdapter.ts
|
|
3015
|
+
var FileSystemError;
|
|
3016
|
+
var init_FileSystemAdapter = __esm({
|
|
3017
|
+
"src/util/migrator/FileSystemAdapter.ts"() {
|
|
3018
|
+
"use strict";
|
|
3019
|
+
FileSystemError = class extends Error {
|
|
3020
|
+
constructor(message, operation, filePath, cause) {
|
|
3021
|
+
super(message);
|
|
3022
|
+
this.operation = operation;
|
|
3023
|
+
this.filePath = filePath;
|
|
3024
|
+
this.cause = cause;
|
|
3025
|
+
this.name = "FileSystemError";
|
|
3026
|
+
}
|
|
3027
|
+
};
|
|
3028
|
+
}
|
|
3029
|
+
});
|
|
3030
|
+
|
|
3031
|
+
// src/util/migrator/validation.ts
|
|
3032
|
+
async function validateStaticCourse(staticPath, fs) {
|
|
3033
|
+
const validation = {
|
|
3034
|
+
valid: true,
|
|
3035
|
+
manifestExists: false,
|
|
3036
|
+
chunksExist: false,
|
|
3037
|
+
attachmentsExist: false,
|
|
3038
|
+
errors: [],
|
|
3039
|
+
warnings: []
|
|
3040
|
+
};
|
|
3041
|
+
try {
|
|
3042
|
+
if (fs) {
|
|
3043
|
+
const stats = await fs.stat(staticPath);
|
|
3044
|
+
if (!stats.isDirectory()) {
|
|
3045
|
+
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3046
|
+
validation.valid = false;
|
|
3047
|
+
return validation;
|
|
3048
|
+
}
|
|
3049
|
+
} else if (!nodeFS) {
|
|
3050
|
+
validation.errors.push("File system access not available - validation skipped");
|
|
3051
|
+
validation.valid = false;
|
|
3052
|
+
return validation;
|
|
3053
|
+
} else {
|
|
3054
|
+
const stats = await nodeFS.promises.stat(staticPath);
|
|
3055
|
+
if (!stats.isDirectory()) {
|
|
3056
|
+
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3057
|
+
validation.valid = false;
|
|
3058
|
+
return validation;
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
let manifestPath = `${staticPath}/manifest.json`;
|
|
3062
|
+
try {
|
|
3063
|
+
if (fs) {
|
|
3064
|
+
manifestPath = fs.joinPath(staticPath, "manifest.json");
|
|
3065
|
+
if (await fs.exists(manifestPath)) {
|
|
3066
|
+
validation.manifestExists = true;
|
|
3067
|
+
const manifestContent = await fs.readFile(manifestPath);
|
|
3068
|
+
const manifest = JSON.parse(manifestContent);
|
|
3069
|
+
validation.courseId = manifest.courseId;
|
|
3070
|
+
validation.courseName = manifest.courseName;
|
|
3071
|
+
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3072
|
+
validation.errors.push("Invalid manifest structure");
|
|
3073
|
+
validation.valid = false;
|
|
3074
|
+
}
|
|
3075
|
+
} else {
|
|
3076
|
+
validation.errors.push(`Manifest not found: ${manifestPath}`);
|
|
3077
|
+
validation.valid = false;
|
|
3078
|
+
}
|
|
3079
|
+
} else {
|
|
3080
|
+
manifestPath = `${staticPath}/manifest.json`;
|
|
3081
|
+
await nodeFS.promises.access(manifestPath);
|
|
3082
|
+
validation.manifestExists = true;
|
|
3083
|
+
const manifestContent = await nodeFS.promises.readFile(manifestPath, "utf8");
|
|
3084
|
+
const manifest = JSON.parse(manifestContent);
|
|
3085
|
+
validation.courseId = manifest.courseId;
|
|
3086
|
+
validation.courseName = manifest.courseName;
|
|
3087
|
+
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3088
|
+
validation.errors.push("Invalid manifest structure");
|
|
3089
|
+
validation.valid = false;
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
} catch (error) {
|
|
3093
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Manifest not found or invalid: ${manifestPath}`;
|
|
3094
|
+
validation.errors.push(errorMessage);
|
|
3095
|
+
validation.valid = false;
|
|
3096
|
+
}
|
|
3097
|
+
let chunksPath = `${staticPath}/chunks`;
|
|
3098
|
+
try {
|
|
3099
|
+
if (fs) {
|
|
3100
|
+
chunksPath = fs.joinPath(staticPath, "chunks");
|
|
3101
|
+
if (await fs.exists(chunksPath)) {
|
|
3102
|
+
const chunksStats = await fs.stat(chunksPath);
|
|
3103
|
+
if (chunksStats.isDirectory()) {
|
|
3104
|
+
validation.chunksExist = true;
|
|
3105
|
+
} else {
|
|
3106
|
+
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3107
|
+
validation.valid = false;
|
|
3108
|
+
}
|
|
3109
|
+
} else {
|
|
3110
|
+
validation.errors.push(`Chunks directory not found: ${chunksPath}`);
|
|
3111
|
+
validation.valid = false;
|
|
3112
|
+
}
|
|
3113
|
+
} else {
|
|
3114
|
+
chunksPath = `${staticPath}/chunks`;
|
|
3115
|
+
const chunksStats = await nodeFS.promises.stat(chunksPath);
|
|
3116
|
+
if (chunksStats.isDirectory()) {
|
|
3117
|
+
validation.chunksExist = true;
|
|
3118
|
+
} else {
|
|
3119
|
+
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3120
|
+
validation.valid = false;
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
} catch (error) {
|
|
3124
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Chunks directory not found: ${chunksPath}`;
|
|
3125
|
+
validation.errors.push(errorMessage);
|
|
3126
|
+
validation.valid = false;
|
|
3127
|
+
}
|
|
3128
|
+
let attachmentsPath;
|
|
3129
|
+
try {
|
|
3130
|
+
if (fs) {
|
|
3131
|
+
attachmentsPath = fs.joinPath(staticPath, "attachments");
|
|
3132
|
+
if (await fs.exists(attachmentsPath)) {
|
|
3133
|
+
const attachmentsStats = await fs.stat(attachmentsPath);
|
|
3134
|
+
if (attachmentsStats.isDirectory()) {
|
|
3135
|
+
validation.attachmentsExist = true;
|
|
3136
|
+
}
|
|
3137
|
+
} else {
|
|
3138
|
+
validation.warnings.push(
|
|
3139
|
+
`Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`
|
|
3140
|
+
);
|
|
3141
|
+
}
|
|
3142
|
+
} else {
|
|
3143
|
+
attachmentsPath = `${staticPath}/attachments`;
|
|
3144
|
+
const attachmentsStats = await nodeFS.promises.stat(attachmentsPath);
|
|
3145
|
+
if (attachmentsStats.isDirectory()) {
|
|
3146
|
+
validation.attachmentsExist = true;
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
} catch (error) {
|
|
3150
|
+
attachmentsPath = attachmentsPath || `${staticPath}/attachments`;
|
|
3151
|
+
const warningMessage = error instanceof FileSystemError ? error.message : `Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`;
|
|
3152
|
+
validation.warnings.push(warningMessage);
|
|
3153
|
+
}
|
|
3154
|
+
} catch (error) {
|
|
3155
|
+
validation.errors.push(
|
|
3156
|
+
`Failed to validate static course: ${error instanceof Error ? error.message : String(error)}`
|
|
3157
|
+
);
|
|
3158
|
+
validation.valid = false;
|
|
3159
|
+
}
|
|
3160
|
+
return validation;
|
|
3161
|
+
}
|
|
3162
|
+
async function validateMigration(targetDB, expectedCounts, manifest) {
|
|
3163
|
+
const validation = {
|
|
3164
|
+
valid: true,
|
|
3165
|
+
documentCountMatch: false,
|
|
3166
|
+
attachmentIntegrity: false,
|
|
3167
|
+
viewFunctionality: false,
|
|
3168
|
+
issues: []
|
|
3169
|
+
};
|
|
3170
|
+
try {
|
|
3171
|
+
logger.info("Starting migration validation...");
|
|
3172
|
+
const actualCounts = await getActualDocumentCounts(targetDB);
|
|
3173
|
+
validation.documentCountMatch = compareDocumentCounts(
|
|
3174
|
+
expectedCounts,
|
|
3175
|
+
actualCounts,
|
|
3176
|
+
validation.issues
|
|
3177
|
+
);
|
|
3178
|
+
await validateCourseConfig(targetDB, manifest, validation.issues);
|
|
3179
|
+
validation.viewFunctionality = await validateViews(targetDB, manifest, validation.issues);
|
|
3180
|
+
validation.attachmentIntegrity = await validateAttachmentIntegrity(targetDB, validation.issues);
|
|
3181
|
+
validation.valid = validation.documentCountMatch && validation.viewFunctionality && validation.attachmentIntegrity;
|
|
3182
|
+
logger.info(`Migration validation completed. Valid: ${validation.valid}`);
|
|
3183
|
+
if (validation.issues.length > 0) {
|
|
3184
|
+
logger.info(`Validation issues: ${validation.issues.length}`);
|
|
3185
|
+
validation.issues.forEach((issue) => {
|
|
3186
|
+
if (issue.type === "error") {
|
|
3187
|
+
logger.error(`${issue.category}: ${issue.message}`);
|
|
3188
|
+
} else {
|
|
3189
|
+
logger.warn(`${issue.category}: ${issue.message}`);
|
|
3190
|
+
}
|
|
3191
|
+
});
|
|
3192
|
+
}
|
|
3193
|
+
} catch (error) {
|
|
3194
|
+
validation.valid = false;
|
|
3195
|
+
validation.issues.push({
|
|
3196
|
+
type: "error",
|
|
3197
|
+
category: "metadata",
|
|
3198
|
+
message: `Validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3199
|
+
});
|
|
3200
|
+
}
|
|
3201
|
+
return validation;
|
|
3202
|
+
}
|
|
3203
|
+
async function getActualDocumentCounts(db) {
|
|
3204
|
+
const counts = {};
|
|
3205
|
+
try {
|
|
3206
|
+
const allDocs = await db.allDocs({ include_docs: true });
|
|
3207
|
+
for (const row of allDocs.rows) {
|
|
3208
|
+
if (row.id.startsWith("_design/")) {
|
|
3209
|
+
counts["_design"] = (counts["_design"] || 0) + 1;
|
|
3210
|
+
continue;
|
|
3211
|
+
}
|
|
3212
|
+
const doc = row.doc;
|
|
3213
|
+
if (doc && doc.docType) {
|
|
3214
|
+
counts[doc.docType] = (counts[doc.docType] || 0) + 1;
|
|
3215
|
+
} else {
|
|
3216
|
+
counts["unknown"] = (counts["unknown"] || 0) + 1;
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
3219
|
+
} catch (error) {
|
|
3220
|
+
logger.error("Failed to get actual document counts:", error);
|
|
3221
|
+
}
|
|
3222
|
+
return counts;
|
|
3223
|
+
}
|
|
3224
|
+
function compareDocumentCounts(expected, actual, issues) {
|
|
3225
|
+
let countsMatch = true;
|
|
3226
|
+
for (const [docType, expectedCount] of Object.entries(expected)) {
|
|
3227
|
+
const actualCount = actual[docType] || 0;
|
|
3228
|
+
if (actualCount !== expectedCount) {
|
|
3229
|
+
countsMatch = false;
|
|
3230
|
+
issues.push({
|
|
3231
|
+
type: "error",
|
|
3232
|
+
category: "documents",
|
|
3233
|
+
message: `Document count mismatch for ${docType}: expected ${expectedCount}, got ${actualCount}`
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
for (const [docType, actualCount] of Object.entries(actual)) {
|
|
3238
|
+
if (!expected[docType] && docType !== "_design") {
|
|
3239
|
+
issues.push({
|
|
3240
|
+
type: "warning",
|
|
3241
|
+
category: "documents",
|
|
3242
|
+
message: `Unexpected document type found: ${docType} (${actualCount} documents)`
|
|
3243
|
+
});
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
return countsMatch;
|
|
3247
|
+
}
|
|
3248
|
+
async function validateCourseConfig(db, manifest, issues) {
|
|
3249
|
+
try {
|
|
3250
|
+
const courseConfig = await db.get("CourseConfig");
|
|
3251
|
+
if (!courseConfig) {
|
|
3252
|
+
issues.push({
|
|
3253
|
+
type: "error",
|
|
3254
|
+
category: "course_config",
|
|
3255
|
+
message: "CourseConfig document not found after migration"
|
|
3256
|
+
});
|
|
3257
|
+
return;
|
|
3258
|
+
}
|
|
3259
|
+
if (!courseConfig.courseID) {
|
|
3260
|
+
issues.push({
|
|
3261
|
+
type: "warning",
|
|
3262
|
+
category: "course_config",
|
|
3263
|
+
message: "CourseConfig document missing courseID field"
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
if (courseConfig.courseID !== manifest.courseId) {
|
|
3267
|
+
issues.push({
|
|
3268
|
+
type: "warning",
|
|
3269
|
+
category: "course_config",
|
|
3270
|
+
message: `CourseConfig courseID mismatch: expected ${manifest.courseId}, got ${courseConfig.courseID}`
|
|
3271
|
+
});
|
|
3272
|
+
}
|
|
3273
|
+
logger.debug("CourseConfig document validation passed");
|
|
3274
|
+
} catch (error) {
|
|
3275
|
+
if (error.status === 404) {
|
|
3276
|
+
issues.push({
|
|
3277
|
+
type: "error",
|
|
3278
|
+
category: "course_config",
|
|
3279
|
+
message: "CourseConfig document not found in database"
|
|
3280
|
+
});
|
|
3281
|
+
} else {
|
|
3282
|
+
issues.push({
|
|
3283
|
+
type: "error",
|
|
3284
|
+
category: "course_config",
|
|
3285
|
+
message: `Failed to validate CourseConfig document: ${error instanceof Error ? error.message : String(error)}`
|
|
3286
|
+
});
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
async function validateViews(db, manifest, issues) {
|
|
3291
|
+
let viewsValid = true;
|
|
3292
|
+
try {
|
|
3293
|
+
for (const designDoc of manifest.designDocs) {
|
|
3294
|
+
try {
|
|
3295
|
+
const doc = await db.get(designDoc._id);
|
|
3296
|
+
if (!doc) {
|
|
3297
|
+
viewsValid = false;
|
|
3298
|
+
issues.push({
|
|
3299
|
+
type: "error",
|
|
3300
|
+
category: "views",
|
|
3301
|
+
message: `Design document not found: ${designDoc._id}`
|
|
3302
|
+
});
|
|
3303
|
+
continue;
|
|
3304
|
+
}
|
|
3305
|
+
for (const viewName of Object.keys(designDoc.views)) {
|
|
3306
|
+
try {
|
|
3307
|
+
const viewPath = `${designDoc._id}/${viewName}`;
|
|
3308
|
+
await db.query(viewPath, { limit: 1 });
|
|
3309
|
+
} catch (viewError) {
|
|
3310
|
+
viewsValid = false;
|
|
3311
|
+
issues.push({
|
|
3312
|
+
type: "error",
|
|
3313
|
+
category: "views",
|
|
3314
|
+
message: `View not accessible: ${designDoc._id}/${viewName} - ${viewError}`
|
|
3315
|
+
});
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
} catch (error) {
|
|
3319
|
+
viewsValid = false;
|
|
3320
|
+
issues.push({
|
|
3321
|
+
type: "error",
|
|
3322
|
+
category: "views",
|
|
3323
|
+
message: `Failed to validate design document ${designDoc._id}: ${error}`
|
|
3324
|
+
});
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
} catch (error) {
|
|
3328
|
+
viewsValid = false;
|
|
3329
|
+
issues.push({
|
|
3330
|
+
type: "error",
|
|
3331
|
+
category: "views",
|
|
3332
|
+
message: `View validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
return viewsValid;
|
|
3336
|
+
}
|
|
3337
|
+
async function validateAttachmentIntegrity(db, issues) {
|
|
3338
|
+
let attachmentsValid = true;
|
|
3339
|
+
try {
|
|
3340
|
+
const allDocs = await db.allDocs({
|
|
3341
|
+
include_docs: true,
|
|
3342
|
+
limit: 10
|
|
3343
|
+
// Sample first 10 documents for performance
|
|
3344
|
+
});
|
|
3345
|
+
let attachmentCount = 0;
|
|
3346
|
+
let validAttachments = 0;
|
|
3347
|
+
for (const row of allDocs.rows) {
|
|
3348
|
+
const doc = row.doc;
|
|
3349
|
+
if (doc && doc._attachments) {
|
|
3350
|
+
for (const [attachmentName, _attachmentMeta] of Object.entries(doc._attachments)) {
|
|
3351
|
+
attachmentCount++;
|
|
3352
|
+
try {
|
|
3353
|
+
const attachment = await db.getAttachment(doc._id, attachmentName);
|
|
3354
|
+
if (attachment) {
|
|
3355
|
+
validAttachments++;
|
|
3356
|
+
}
|
|
3357
|
+
} catch (attachmentError) {
|
|
3358
|
+
attachmentsValid = false;
|
|
3359
|
+
issues.push({
|
|
3360
|
+
type: "error",
|
|
3361
|
+
category: "attachments",
|
|
3362
|
+
message: `Attachment not accessible: ${doc._id}/${attachmentName} - ${attachmentError}`
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
if (attachmentCount === 0) {
|
|
3369
|
+
issues.push({
|
|
3370
|
+
type: "warning",
|
|
3371
|
+
category: "attachments",
|
|
3372
|
+
message: "No attachments found in sampled documents"
|
|
3373
|
+
});
|
|
3374
|
+
} else {
|
|
3375
|
+
logger.info(`Validated ${validAttachments}/${attachmentCount} sampled attachments`);
|
|
3376
|
+
}
|
|
3377
|
+
} catch (error) {
|
|
3378
|
+
attachmentsValid = false;
|
|
3379
|
+
issues.push({
|
|
3380
|
+
type: "error",
|
|
3381
|
+
category: "attachments",
|
|
3382
|
+
message: `Attachment validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3383
|
+
});
|
|
3384
|
+
}
|
|
3385
|
+
return attachmentsValid;
|
|
3386
|
+
}
|
|
3387
|
+
var nodeFS;
|
|
3388
|
+
var init_validation = __esm({
|
|
3389
|
+
"src/util/migrator/validation.ts"() {
|
|
3390
|
+
"use strict";
|
|
3391
|
+
init_logger();
|
|
3392
|
+
init_FileSystemAdapter();
|
|
3393
|
+
nodeFS = null;
|
|
3394
|
+
try {
|
|
3395
|
+
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3396
|
+
nodeFS = eval("require")("fs");
|
|
3397
|
+
nodeFS.promises = nodeFS.promises || eval("require")("fs").promises;
|
|
3398
|
+
}
|
|
3399
|
+
} catch {
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
});
|
|
3403
|
+
|
|
3404
|
+
// src/util/migrator/StaticToCouchDBMigrator.ts
|
|
3405
|
+
var nodeFS2, nodePath, StaticToCouchDBMigrator;
|
|
3406
|
+
var init_StaticToCouchDBMigrator = __esm({
|
|
3407
|
+
"src/util/migrator/StaticToCouchDBMigrator.ts"() {
|
|
3408
|
+
"use strict";
|
|
3409
|
+
init_logger();
|
|
3410
|
+
init_types4();
|
|
3411
|
+
init_validation();
|
|
3412
|
+
init_FileSystemAdapter();
|
|
3413
|
+
nodeFS2 = null;
|
|
3414
|
+
nodePath = null;
|
|
3415
|
+
try {
|
|
3416
|
+
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3417
|
+
nodeFS2 = eval("require")("fs");
|
|
3418
|
+
nodePath = eval("require")("path");
|
|
3419
|
+
nodeFS2.promises = nodeFS2.promises || eval("require")("fs").promises;
|
|
3420
|
+
}
|
|
3421
|
+
} catch {
|
|
3422
|
+
}
|
|
3423
|
+
StaticToCouchDBMigrator = class {
|
|
3424
|
+
options;
|
|
3425
|
+
progressCallback;
|
|
3426
|
+
fs;
|
|
3427
|
+
constructor(options = {}, fileSystemAdapter) {
|
|
3428
|
+
this.options = {
|
|
3429
|
+
...DEFAULT_MIGRATION_OPTIONS,
|
|
3430
|
+
...options
|
|
3431
|
+
};
|
|
3432
|
+
this.fs = fileSystemAdapter;
|
|
3433
|
+
}
|
|
3434
|
+
/**
|
|
3435
|
+
* Set a progress callback to receive updates during migration
|
|
3436
|
+
*/
|
|
3437
|
+
setProgressCallback(callback) {
|
|
3438
|
+
this.progressCallback = callback;
|
|
3439
|
+
}
|
|
3440
|
+
/**
|
|
3441
|
+
* Migrate a static course to CouchDB
|
|
3442
|
+
*/
|
|
3443
|
+
async migrateCourse(staticPath, targetDB) {
|
|
3444
|
+
const startTime = Date.now();
|
|
3445
|
+
const result = {
|
|
3446
|
+
success: false,
|
|
3447
|
+
documentsRestored: 0,
|
|
3448
|
+
attachmentsRestored: 0,
|
|
3449
|
+
designDocsRestored: 0,
|
|
3450
|
+
courseConfigRestored: 0,
|
|
3451
|
+
errors: [],
|
|
3452
|
+
warnings: [],
|
|
3453
|
+
migrationTime: 0
|
|
3454
|
+
};
|
|
3455
|
+
try {
|
|
3456
|
+
logger.info(`Starting migration from ${staticPath} to CouchDB`);
|
|
3457
|
+
this.reportProgress("manifest", 0, 1, "Validating static course...");
|
|
3458
|
+
const validation = await validateStaticCourse(staticPath, this.fs);
|
|
3459
|
+
if (!validation.valid) {
|
|
3460
|
+
result.errors.push(...validation.errors);
|
|
3461
|
+
throw new Error(`Static course validation failed: ${validation.errors.join(", ")}`);
|
|
3462
|
+
}
|
|
3463
|
+
result.warnings.push(...validation.warnings);
|
|
3464
|
+
this.reportProgress("manifest", 1, 1, "Loading course manifest...");
|
|
3465
|
+
const manifest = await this.loadManifest(staticPath);
|
|
3466
|
+
logger.info(`Loaded manifest for course: ${manifest.courseId} (${manifest.courseName})`);
|
|
3467
|
+
this.reportProgress(
|
|
3468
|
+
"design_docs",
|
|
3469
|
+
0,
|
|
3470
|
+
manifest.designDocs.length,
|
|
3471
|
+
"Restoring design documents..."
|
|
3472
|
+
);
|
|
3473
|
+
const designDocResults = await this.restoreDesignDocuments(manifest.designDocs, targetDB);
|
|
3474
|
+
result.designDocsRestored = designDocResults.restored;
|
|
3475
|
+
result.errors.push(...designDocResults.errors);
|
|
3476
|
+
result.warnings.push(...designDocResults.warnings);
|
|
3477
|
+
this.reportProgress("course_config", 0, 1, "Restoring CourseConfig document...");
|
|
3478
|
+
const courseConfigResults = await this.restoreCourseConfig(manifest, targetDB);
|
|
3479
|
+
result.courseConfigRestored = courseConfigResults.restored;
|
|
3480
|
+
result.errors.push(...courseConfigResults.errors);
|
|
3481
|
+
result.warnings.push(...courseConfigResults.warnings);
|
|
3482
|
+
this.reportProgress("course_config", 1, 1, "CourseConfig document restored");
|
|
3483
|
+
const expectedCounts = this.calculateExpectedCounts(manifest);
|
|
3484
|
+
this.reportProgress(
|
|
3485
|
+
"documents",
|
|
3486
|
+
0,
|
|
3487
|
+
manifest.documentCount,
|
|
3488
|
+
"Aggregating documents from chunks..."
|
|
3489
|
+
);
|
|
3490
|
+
const documents = await this.aggregateDocuments(staticPath, manifest);
|
|
3491
|
+
const filteredDocuments = documents.filter((doc) => doc._id !== "CourseConfig");
|
|
3492
|
+
if (documents.length !== filteredDocuments.length) {
|
|
3493
|
+
result.warnings.push(
|
|
3494
|
+
`Filtered out ${documents.length - filteredDocuments.length} CourseConfig document(s) from chunks to prevent conflicts`
|
|
3495
|
+
);
|
|
3496
|
+
}
|
|
3497
|
+
this.reportProgress(
|
|
3498
|
+
"documents",
|
|
3499
|
+
filteredDocuments.length,
|
|
3500
|
+
manifest.documentCount,
|
|
3501
|
+
"Uploading documents to CouchDB..."
|
|
3502
|
+
);
|
|
3503
|
+
const docResults = await this.uploadDocuments(filteredDocuments, targetDB);
|
|
3504
|
+
result.documentsRestored = docResults.restored;
|
|
3505
|
+
result.errors.push(...docResults.errors);
|
|
3506
|
+
result.warnings.push(...docResults.warnings);
|
|
3507
|
+
const docsWithAttachments = documents.filter(
|
|
3508
|
+
(doc) => doc._attachments && Object.keys(doc._attachments).length > 0
|
|
3509
|
+
);
|
|
3510
|
+
this.reportProgress("attachments", 0, docsWithAttachments.length, "Uploading attachments...");
|
|
3511
|
+
const attachmentResults = await this.uploadAttachments(
|
|
3512
|
+
staticPath,
|
|
3513
|
+
docsWithAttachments,
|
|
3514
|
+
targetDB
|
|
3515
|
+
);
|
|
3516
|
+
result.attachmentsRestored = attachmentResults.restored;
|
|
3517
|
+
result.errors.push(...attachmentResults.errors);
|
|
3518
|
+
result.warnings.push(...attachmentResults.warnings);
|
|
3519
|
+
if (this.options.validateRoundTrip) {
|
|
3520
|
+
this.reportProgress("validation", 0, 1, "Validating migration...");
|
|
3521
|
+
const validationResult = await validateMigration(targetDB, expectedCounts, manifest);
|
|
3522
|
+
if (!validationResult.valid) {
|
|
3523
|
+
result.warnings.push("Migration validation found issues");
|
|
3524
|
+
validationResult.issues.forEach((issue) => {
|
|
3525
|
+
if (issue.type === "error") {
|
|
3526
|
+
result.errors.push(`Validation: ${issue.message}`);
|
|
3527
|
+
} else {
|
|
3528
|
+
result.warnings.push(`Validation: ${issue.message}`);
|
|
3529
|
+
}
|
|
3530
|
+
});
|
|
3531
|
+
}
|
|
3532
|
+
this.reportProgress("validation", 1, 1, "Migration validation completed");
|
|
3533
|
+
}
|
|
3534
|
+
result.success = result.errors.length === 0;
|
|
3535
|
+
result.migrationTime = Date.now() - startTime;
|
|
3536
|
+
logger.info(`Migration completed in ${result.migrationTime}ms`);
|
|
3537
|
+
logger.info(`Documents restored: ${result.documentsRestored}`);
|
|
3538
|
+
logger.info(`Attachments restored: ${result.attachmentsRestored}`);
|
|
3539
|
+
logger.info(`Design docs restored: ${result.designDocsRestored}`);
|
|
3540
|
+
logger.info(`CourseConfig restored: ${result.courseConfigRestored}`);
|
|
3541
|
+
if (result.errors.length > 0) {
|
|
3542
|
+
logger.error(`Migration completed with ${result.errors.length} errors`);
|
|
3543
|
+
}
|
|
3544
|
+
if (result.warnings.length > 0) {
|
|
3545
|
+
logger.warn(`Migration completed with ${result.warnings.length} warnings`);
|
|
3546
|
+
}
|
|
3547
|
+
} catch (error) {
|
|
3548
|
+
result.success = false;
|
|
3549
|
+
result.migrationTime = Date.now() - startTime;
|
|
3550
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3551
|
+
result.errors.push(`Migration failed: ${errorMessage}`);
|
|
3552
|
+
logger.error("Migration failed:", error);
|
|
3553
|
+
if (this.options.cleanupOnFailure) {
|
|
3554
|
+
try {
|
|
3555
|
+
await this.cleanupFailedMigration(targetDB);
|
|
3556
|
+
} catch (cleanupError) {
|
|
3557
|
+
logger.error("Failed to cleanup after migration failure:", cleanupError);
|
|
3558
|
+
result.warnings.push("Failed to cleanup after migration failure");
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
return result;
|
|
3563
|
+
}
|
|
3564
|
+
/**
|
|
3565
|
+
* Load and parse the manifest file
|
|
3566
|
+
*/
|
|
3567
|
+
async loadManifest(staticPath) {
|
|
3568
|
+
try {
|
|
3569
|
+
let manifestContent;
|
|
3570
|
+
let manifestPath;
|
|
3571
|
+
if (this.fs) {
|
|
3572
|
+
manifestPath = this.fs.joinPath(staticPath, "manifest.json");
|
|
3573
|
+
manifestContent = await this.fs.readFile(manifestPath);
|
|
3574
|
+
} else {
|
|
3575
|
+
manifestPath = nodeFS2 && nodePath ? nodePath.join(staticPath, "manifest.json") : `${staticPath}/manifest.json`;
|
|
3576
|
+
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
3577
|
+
manifestContent = await nodeFS2.promises.readFile(manifestPath, "utf8");
|
|
3578
|
+
} else {
|
|
3579
|
+
const response = await fetch(manifestPath);
|
|
3580
|
+
if (!response.ok) {
|
|
3581
|
+
throw new Error(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
|
|
3582
|
+
}
|
|
3583
|
+
manifestContent = await response.text();
|
|
3584
|
+
}
|
|
3585
|
+
}
|
|
3586
|
+
const manifest = JSON.parse(manifestContent);
|
|
3587
|
+
if (!manifest.version || !manifest.courseId || !manifest.chunks) {
|
|
3588
|
+
throw new Error("Invalid manifest structure");
|
|
3589
|
+
}
|
|
3590
|
+
return manifest;
|
|
3591
|
+
} catch (error) {
|
|
3592
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load manifest: ${error instanceof Error ? error.message : String(error)}`;
|
|
3593
|
+
throw new Error(errorMessage);
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
/**
|
|
3597
|
+
* Restore design documents to CouchDB
|
|
3598
|
+
*/
|
|
3599
|
+
async restoreDesignDocuments(designDocs, db) {
|
|
3600
|
+
const result = { restored: 0, errors: [], warnings: [] };
|
|
3601
|
+
for (let i = 0; i < designDocs.length; i++) {
|
|
3602
|
+
const designDoc = designDocs[i];
|
|
3603
|
+
this.reportProgress("design_docs", i, designDocs.length, `Restoring ${designDoc._id}...`);
|
|
3604
|
+
try {
|
|
3605
|
+
let existingDoc;
|
|
3606
|
+
try {
|
|
3607
|
+
existingDoc = await db.get(designDoc._id);
|
|
3608
|
+
} catch {
|
|
3609
|
+
}
|
|
3610
|
+
const docToInsert = {
|
|
3611
|
+
_id: designDoc._id,
|
|
3612
|
+
views: designDoc.views
|
|
3613
|
+
};
|
|
3614
|
+
if (existingDoc) {
|
|
3615
|
+
docToInsert._rev = existingDoc._rev;
|
|
3616
|
+
logger.debug(`Updating existing design document: ${designDoc._id}`);
|
|
3617
|
+
} else {
|
|
3618
|
+
logger.debug(`Creating new design document: ${designDoc._id}`);
|
|
3619
|
+
}
|
|
3620
|
+
await db.put(docToInsert);
|
|
3621
|
+
result.restored++;
|
|
3622
|
+
} catch (error) {
|
|
3623
|
+
const errorMessage = `Failed to restore design document ${designDoc._id}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3624
|
+
result.errors.push(errorMessage);
|
|
3625
|
+
logger.error(errorMessage);
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
this.reportProgress(
|
|
3629
|
+
"design_docs",
|
|
3630
|
+
designDocs.length,
|
|
3631
|
+
designDocs.length,
|
|
3632
|
+
`Restored ${result.restored} design documents`
|
|
3633
|
+
);
|
|
3634
|
+
return result;
|
|
3635
|
+
}
|
|
3636
|
+
/**
|
|
3637
|
+
* Aggregate documents from all chunks
|
|
3638
|
+
*/
|
|
3639
|
+
async aggregateDocuments(staticPath, manifest) {
|
|
3640
|
+
const allDocuments = [];
|
|
3641
|
+
const documentMap = /* @__PURE__ */ new Map();
|
|
3642
|
+
for (let i = 0; i < manifest.chunks.length; i++) {
|
|
3643
|
+
const chunk = manifest.chunks[i];
|
|
3644
|
+
this.reportProgress(
|
|
3645
|
+
"documents",
|
|
3646
|
+
allDocuments.length,
|
|
3647
|
+
manifest.documentCount,
|
|
3648
|
+
`Loading chunk ${chunk.id}...`
|
|
3649
|
+
);
|
|
3650
|
+
try {
|
|
3651
|
+
const documents = await this.loadChunk(staticPath, chunk);
|
|
3652
|
+
for (const doc of documents) {
|
|
3653
|
+
if (!doc._id) {
|
|
3654
|
+
logger.warn(`Document without _id found in chunk ${chunk.id}, skipping`);
|
|
3655
|
+
continue;
|
|
3656
|
+
}
|
|
3657
|
+
if (documentMap.has(doc._id)) {
|
|
3658
|
+
logger.warn(`Duplicate document ID found: ${doc._id}, using latest version`);
|
|
3659
|
+
}
|
|
3660
|
+
documentMap.set(doc._id, doc);
|
|
3661
|
+
}
|
|
3662
|
+
} catch (error) {
|
|
3663
|
+
throw new Error(
|
|
3664
|
+
`Failed to load chunk ${chunk.id}: ${error instanceof Error ? error.message : String(error)}`
|
|
3665
|
+
);
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
allDocuments.push(...documentMap.values());
|
|
3669
|
+
logger.info(
|
|
3670
|
+
`Aggregated ${allDocuments.length} unique documents from ${manifest.chunks.length} chunks`
|
|
3671
|
+
);
|
|
3672
|
+
return allDocuments;
|
|
3673
|
+
}
|
|
3674
|
+
/**
|
|
3675
|
+
* Load documents from a single chunk file
|
|
3676
|
+
*/
|
|
3677
|
+
async loadChunk(staticPath, chunk) {
|
|
3678
|
+
try {
|
|
3679
|
+
let chunkContent;
|
|
3680
|
+
let chunkPath;
|
|
3681
|
+
if (this.fs) {
|
|
3682
|
+
chunkPath = this.fs.joinPath(staticPath, chunk.path);
|
|
3683
|
+
chunkContent = await this.fs.readFile(chunkPath);
|
|
3684
|
+
} else {
|
|
3685
|
+
chunkPath = nodeFS2 && nodePath ? nodePath.join(staticPath, chunk.path) : `${staticPath}/${chunk.path}`;
|
|
3686
|
+
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
3687
|
+
chunkContent = await nodeFS2.promises.readFile(chunkPath, "utf8");
|
|
3688
|
+
} else {
|
|
3689
|
+
const response = await fetch(chunkPath);
|
|
3690
|
+
if (!response.ok) {
|
|
3691
|
+
throw new Error(`Failed to fetch chunk: ${response.status} ${response.statusText}`);
|
|
3692
|
+
}
|
|
3693
|
+
chunkContent = await response.text();
|
|
3694
|
+
}
|
|
3695
|
+
}
|
|
3696
|
+
const documents = JSON.parse(chunkContent);
|
|
3697
|
+
if (!Array.isArray(documents)) {
|
|
3698
|
+
throw new Error("Chunk file does not contain an array of documents");
|
|
3699
|
+
}
|
|
3700
|
+
return documents;
|
|
3701
|
+
} catch (error) {
|
|
3702
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load chunk: ${error instanceof Error ? error.message : String(error)}`;
|
|
3703
|
+
throw new Error(errorMessage);
|
|
3704
|
+
}
|
|
3705
|
+
}
|
|
3706
|
+
/**
|
|
3707
|
+
* Upload documents to CouchDB in batches
|
|
3708
|
+
*/
|
|
3709
|
+
async uploadDocuments(documents, db) {
|
|
3710
|
+
const result = { restored: 0, errors: [], warnings: [] };
|
|
3711
|
+
const batchSize = this.options.chunkBatchSize;
|
|
3712
|
+
for (let i = 0; i < documents.length; i += batchSize) {
|
|
3713
|
+
const batch = documents.slice(i, i + batchSize);
|
|
3714
|
+
this.reportProgress(
|
|
3715
|
+
"documents",
|
|
3716
|
+
i,
|
|
3717
|
+
documents.length,
|
|
3718
|
+
`Uploading batch ${Math.floor(i / batchSize) + 1}...`
|
|
3719
|
+
);
|
|
3720
|
+
try {
|
|
3721
|
+
const docsToInsert = batch.map((doc) => {
|
|
3722
|
+
const cleanDoc = { ...doc };
|
|
3723
|
+
delete cleanDoc._rev;
|
|
3724
|
+
delete cleanDoc._attachments;
|
|
3725
|
+
return cleanDoc;
|
|
3726
|
+
});
|
|
3727
|
+
const bulkResult = await db.bulkDocs(docsToInsert);
|
|
3728
|
+
for (let j = 0; j < bulkResult.length; j++) {
|
|
3729
|
+
const docResult = bulkResult[j];
|
|
3730
|
+
const originalDoc = batch[j];
|
|
3731
|
+
if ("error" in docResult) {
|
|
3732
|
+
const errorMessage = `Failed to upload document ${originalDoc._id}: ${docResult.error} - ${docResult.reason}`;
|
|
3733
|
+
result.errors.push(errorMessage);
|
|
3734
|
+
logger.error(errorMessage);
|
|
3735
|
+
} else {
|
|
3736
|
+
result.restored++;
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
} catch (error) {
|
|
3740
|
+
let errorMessage;
|
|
3741
|
+
if (error instanceof Error) {
|
|
3742
|
+
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
3743
|
+
} else if (error && typeof error === "object" && "message" in error) {
|
|
3744
|
+
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
3745
|
+
} else {
|
|
3746
|
+
errorMessage = `Failed to upload document batch starting at index ${i}: ${JSON.stringify(error)}`;
|
|
3747
|
+
}
|
|
3748
|
+
result.errors.push(errorMessage);
|
|
3749
|
+
logger.error(errorMessage);
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3752
|
+
this.reportProgress(
|
|
3753
|
+
"documents",
|
|
3754
|
+
documents.length,
|
|
3755
|
+
documents.length,
|
|
3756
|
+
`Uploaded ${result.restored} documents`
|
|
3757
|
+
);
|
|
3758
|
+
return result;
|
|
3759
|
+
}
|
|
3760
|
+
/**
|
|
3761
|
+
* Upload attachments from filesystem to CouchDB
|
|
3762
|
+
*/
|
|
3763
|
+
async uploadAttachments(staticPath, documents, db) {
|
|
3764
|
+
const result = { restored: 0, errors: [], warnings: [] };
|
|
3765
|
+
let processedDocs = 0;
|
|
3766
|
+
for (const doc of documents) {
|
|
3767
|
+
this.reportProgress(
|
|
3768
|
+
"attachments",
|
|
3769
|
+
processedDocs,
|
|
3770
|
+
documents.length,
|
|
3771
|
+
`Processing attachments for ${doc._id}...`
|
|
3772
|
+
);
|
|
3773
|
+
processedDocs++;
|
|
3774
|
+
if (!doc._attachments) {
|
|
3775
|
+
continue;
|
|
3776
|
+
}
|
|
3777
|
+
for (const [attachmentName, attachmentMeta] of Object.entries(doc._attachments)) {
|
|
3778
|
+
try {
|
|
3779
|
+
const uploadResult = await this.uploadSingleAttachment(
|
|
3780
|
+
staticPath,
|
|
3781
|
+
doc._id,
|
|
3782
|
+
attachmentName,
|
|
3783
|
+
attachmentMeta,
|
|
3784
|
+
db
|
|
3785
|
+
);
|
|
3786
|
+
if (uploadResult.success) {
|
|
3787
|
+
result.restored++;
|
|
3788
|
+
} else {
|
|
3789
|
+
result.errors.push(uploadResult.error || "Unknown attachment upload error");
|
|
3790
|
+
}
|
|
3791
|
+
} catch (error) {
|
|
3792
|
+
const errorMessage = `Failed to upload attachment ${doc._id}/${attachmentName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3793
|
+
result.errors.push(errorMessage);
|
|
3794
|
+
logger.error(errorMessage);
|
|
3795
|
+
}
|
|
3796
|
+
}
|
|
3797
|
+
}
|
|
3798
|
+
this.reportProgress(
|
|
3799
|
+
"attachments",
|
|
3800
|
+
documents.length,
|
|
3801
|
+
documents.length,
|
|
3802
|
+
`Uploaded ${result.restored} attachments`
|
|
3803
|
+
);
|
|
3804
|
+
return result;
|
|
3805
|
+
}
|
|
3806
|
+
/**
|
|
3807
|
+
* Upload a single attachment file
|
|
3808
|
+
*/
|
|
3809
|
+
async uploadSingleAttachment(staticPath, docId, attachmentName, attachmentMeta, db) {
|
|
3810
|
+
const result = {
|
|
3811
|
+
success: false,
|
|
3812
|
+
attachmentName,
|
|
3813
|
+
docId
|
|
3814
|
+
};
|
|
3815
|
+
try {
|
|
3816
|
+
if (!attachmentMeta.path) {
|
|
3817
|
+
result.error = "Attachment metadata missing file path";
|
|
3818
|
+
return result;
|
|
3819
|
+
}
|
|
3820
|
+
let attachmentData;
|
|
3821
|
+
let attachmentPath;
|
|
3822
|
+
if (this.fs) {
|
|
3823
|
+
attachmentPath = this.fs.joinPath(staticPath, attachmentMeta.path);
|
|
3824
|
+
attachmentData = await this.fs.readBinary(attachmentPath);
|
|
3825
|
+
} else {
|
|
3826
|
+
attachmentPath = nodeFS2 && nodePath ? nodePath.join(staticPath, attachmentMeta.path) : `${staticPath}/${attachmentMeta.path}`;
|
|
3827
|
+
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
3828
|
+
attachmentData = await nodeFS2.promises.readFile(attachmentPath);
|
|
3829
|
+
} else {
|
|
3830
|
+
const response = await fetch(attachmentPath);
|
|
3831
|
+
if (!response.ok) {
|
|
3832
|
+
result.error = `Failed to fetch attachment: ${response.status} ${response.statusText}`;
|
|
3833
|
+
return result;
|
|
3834
|
+
}
|
|
3835
|
+
attachmentData = await response.arrayBuffer();
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
const doc = await db.get(docId);
|
|
3839
|
+
await db.putAttachment(
|
|
3840
|
+
docId,
|
|
3841
|
+
attachmentName,
|
|
3842
|
+
doc._rev,
|
|
3843
|
+
attachmentData,
|
|
3844
|
+
// PouchDB accepts both ArrayBuffer and Buffer
|
|
3845
|
+
attachmentMeta.content_type
|
|
3846
|
+
);
|
|
3847
|
+
result.success = true;
|
|
3848
|
+
} catch (error) {
|
|
3849
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
3850
|
+
}
|
|
3851
|
+
return result;
|
|
3852
|
+
}
|
|
3853
|
+
/**
|
|
3854
|
+
* Restore CourseConfig document from manifest
|
|
3855
|
+
*/
|
|
3856
|
+
async restoreCourseConfig(manifest, targetDB) {
|
|
3857
|
+
const results = {
|
|
3858
|
+
restored: 0,
|
|
3859
|
+
errors: [],
|
|
3860
|
+
warnings: []
|
|
3861
|
+
};
|
|
3862
|
+
try {
|
|
3863
|
+
if (!manifest.courseConfig) {
|
|
3864
|
+
results.warnings.push(
|
|
3865
|
+
"No courseConfig found in manifest, skipping CourseConfig document creation"
|
|
3866
|
+
);
|
|
3867
|
+
return results;
|
|
3868
|
+
}
|
|
3869
|
+
const courseConfigDoc = {
|
|
3870
|
+
_id: "CourseConfig",
|
|
3871
|
+
...manifest.courseConfig,
|
|
3872
|
+
courseID: manifest.courseId
|
|
3873
|
+
};
|
|
3874
|
+
delete courseConfigDoc._rev;
|
|
3875
|
+
await targetDB.put(courseConfigDoc);
|
|
3876
|
+
results.restored = 1;
|
|
3877
|
+
logger.info(`CourseConfig document created for course: ${manifest.courseId}`);
|
|
3878
|
+
} catch (error) {
|
|
3879
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
3880
|
+
results.errors.push(`Failed to restore CourseConfig: ${errorMessage}`);
|
|
3881
|
+
logger.error("CourseConfig restoration failed:", error);
|
|
3882
|
+
}
|
|
3883
|
+
return results;
|
|
3884
|
+
}
|
|
3885
|
+
/**
|
|
3886
|
+
* Calculate expected document counts from manifest
|
|
3887
|
+
*/
|
|
3888
|
+
calculateExpectedCounts(manifest) {
|
|
3889
|
+
const counts = {};
|
|
3890
|
+
for (const chunk of manifest.chunks) {
|
|
3891
|
+
counts[chunk.docType] = (counts[chunk.docType] || 0) + chunk.documentCount;
|
|
3892
|
+
}
|
|
3893
|
+
if (manifest.designDocs.length > 0) {
|
|
3894
|
+
counts["_design"] = manifest.designDocs.length;
|
|
3895
|
+
}
|
|
3896
|
+
return counts;
|
|
3897
|
+
}
|
|
3898
|
+
/**
|
|
3899
|
+
* Clean up database after failed migration
|
|
3900
|
+
*/
|
|
3901
|
+
async cleanupFailedMigration(db) {
|
|
3902
|
+
logger.info("Cleaning up failed migration...");
|
|
3903
|
+
try {
|
|
3904
|
+
const allDocs = await db.allDocs();
|
|
3905
|
+
const docsToDelete = allDocs.rows.map((row) => ({
|
|
3906
|
+
_id: row.id,
|
|
3907
|
+
_rev: row.value.rev,
|
|
3908
|
+
_deleted: true
|
|
3909
|
+
}));
|
|
3910
|
+
if (docsToDelete.length > 0) {
|
|
3911
|
+
await db.bulkDocs(docsToDelete);
|
|
3912
|
+
logger.info(`Cleaned up ${docsToDelete.length} documents from failed migration`);
|
|
3913
|
+
}
|
|
3914
|
+
} catch (error) {
|
|
3915
|
+
logger.error("Failed to cleanup documents:", error);
|
|
3916
|
+
throw error;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
/**
|
|
3920
|
+
* Report progress to callback if available
|
|
3921
|
+
*/
|
|
3922
|
+
reportProgress(phase, current, total, message) {
|
|
3923
|
+
if (this.progressCallback) {
|
|
3924
|
+
this.progressCallback({
|
|
3925
|
+
phase,
|
|
3926
|
+
current,
|
|
3927
|
+
total,
|
|
3928
|
+
message
|
|
3929
|
+
});
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
/**
|
|
3933
|
+
* Check if a path is a local file path (vs URL)
|
|
3934
|
+
*/
|
|
3935
|
+
isLocalPath(path2) {
|
|
3936
|
+
return !path2.startsWith("http://") && !path2.startsWith("https://");
|
|
3937
|
+
}
|
|
3938
|
+
};
|
|
3939
|
+
}
|
|
3940
|
+
});
|
|
3941
|
+
|
|
3942
|
+
// src/util/migrator/index.ts
|
|
3943
|
+
var init_migrator = __esm({
|
|
3944
|
+
"src/util/migrator/index.ts"() {
|
|
3945
|
+
"use strict";
|
|
3946
|
+
init_StaticToCouchDBMigrator();
|
|
3947
|
+
init_validation();
|
|
3948
|
+
init_FileSystemAdapter();
|
|
3949
|
+
}
|
|
3950
|
+
});
|
|
3951
|
+
|
|
3952
|
+
// src/util/index.ts
|
|
3953
|
+
var init_util2 = __esm({
|
|
3954
|
+
"src/util/index.ts"() {
|
|
3955
|
+
"use strict";
|
|
3956
|
+
init_Loggable();
|
|
3957
|
+
init_packer();
|
|
3958
|
+
init_migrator();
|
|
3959
|
+
init_dataDirectory();
|
|
3960
|
+
}
|
|
3961
|
+
});
|
|
3962
|
+
|
|
3963
|
+
// src/study/SourceMixer.ts
|
|
3964
|
+
var init_SourceMixer = __esm({
|
|
3965
|
+
"src/study/SourceMixer.ts"() {
|
|
3966
|
+
"use strict";
|
|
3967
|
+
}
|
|
3968
|
+
});
|
|
3969
|
+
|
|
3970
|
+
// src/study/MixerDebugger.ts
|
|
3971
|
+
function printMixerSummary(run) {
|
|
3972
|
+
console.group(`\u{1F3A8} Mixer Run: ${run.mixerType}`);
|
|
3973
|
+
logger.info(`Run ID: ${run.runId}`);
|
|
3974
|
+
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
3975
|
+
logger.info(
|
|
3976
|
+
`Config: limit=${run.requestedLimit}${run.quotaPerSource ? `, quota/source=${run.quotaPerSource}` : ""}`
|
|
3977
|
+
);
|
|
3978
|
+
console.group(`\u{1F4E5} Input: ${run.sourceSummaries.length} sources`);
|
|
3979
|
+
for (const src of run.sourceSummaries) {
|
|
3980
|
+
logger.info(
|
|
3981
|
+
` ${src.sourceName || src.sourceId}: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`
|
|
3982
|
+
);
|
|
3983
|
+
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}], avg: ${src.avgScore.toFixed(2)}`);
|
|
3984
|
+
}
|
|
3985
|
+
console.groupEnd();
|
|
3986
|
+
console.group(`\u{1F4E4} Output: ${run.finalCount} cards selected (${run.reviewsSelected} reviews, ${run.newSelected} new)`);
|
|
3987
|
+
for (const breakdown of run.sourceBreakdowns) {
|
|
3988
|
+
const name = breakdown.sourceName || breakdown.sourceId;
|
|
3989
|
+
logger.info(
|
|
3990
|
+
` ${name}: ${breakdown.totalSelected} selected (${breakdown.reviewsSelected} reviews, ${breakdown.newSelected} new) - ${breakdown.selectionRate.toFixed(1)}% selection rate`
|
|
3991
|
+
);
|
|
3992
|
+
}
|
|
3993
|
+
console.groupEnd();
|
|
3994
|
+
console.groupEnd();
|
|
3995
|
+
}
|
|
3996
|
+
function mountMixerDebugger() {
|
|
3997
|
+
if (typeof window === "undefined") return;
|
|
3998
|
+
const win = window;
|
|
3999
|
+
win.skuilder = win.skuilder || {};
|
|
4000
|
+
win.skuilder.mixer = mixerDebugAPI;
|
|
4001
|
+
}
|
|
4002
|
+
var runHistory2, mixerDebugAPI;
|
|
4003
|
+
var init_MixerDebugger = __esm({
|
|
4004
|
+
"src/study/MixerDebugger.ts"() {
|
|
4005
|
+
"use strict";
|
|
4006
|
+
init_logger();
|
|
4007
|
+
init_navigators();
|
|
4008
|
+
runHistory2 = [];
|
|
4009
|
+
mixerDebugAPI = {
|
|
4010
|
+
/**
|
|
4011
|
+
* Get raw run history for programmatic access.
|
|
4012
|
+
*/
|
|
4013
|
+
get runs() {
|
|
4014
|
+
return [...runHistory2];
|
|
4015
|
+
},
|
|
4016
|
+
/**
|
|
4017
|
+
* Show summary of a specific mixer run.
|
|
4018
|
+
*/
|
|
4019
|
+
showRun(idOrIndex = 0) {
|
|
4020
|
+
if (runHistory2.length === 0) {
|
|
4021
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4022
|
+
return;
|
|
4023
|
+
}
|
|
4024
|
+
let run;
|
|
4025
|
+
if (typeof idOrIndex === "number") {
|
|
4026
|
+
run = runHistory2[idOrIndex];
|
|
4027
|
+
if (!run) {
|
|
4028
|
+
logger.info(`[Mixer Debug] No run found at index ${idOrIndex}. History length: ${runHistory2.length}`);
|
|
4029
|
+
return;
|
|
4030
|
+
}
|
|
4031
|
+
} else {
|
|
4032
|
+
run = runHistory2.find((r) => r.runId.endsWith(idOrIndex));
|
|
4033
|
+
if (!run) {
|
|
4034
|
+
logger.info(`[Mixer Debug] No run found matching ID '${idOrIndex}'.`);
|
|
4035
|
+
return;
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
printMixerSummary(run);
|
|
4039
|
+
},
|
|
4040
|
+
/**
|
|
4041
|
+
* Show summary of the last mixer run.
|
|
4042
|
+
*/
|
|
4043
|
+
showLastMix() {
|
|
4044
|
+
this.showRun(0);
|
|
4045
|
+
},
|
|
4046
|
+
/**
|
|
4047
|
+
* Explain source balance in the last run.
|
|
4048
|
+
*/
|
|
4049
|
+
explainSourceBalance() {
|
|
4050
|
+
if (runHistory2.length === 0) {
|
|
4051
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4052
|
+
return;
|
|
4053
|
+
}
|
|
4054
|
+
const run = runHistory2[0];
|
|
4055
|
+
console.group("\u2696\uFE0F Source Balance Analysis");
|
|
4056
|
+
logger.info(`Mixer: ${run.mixerType}`);
|
|
4057
|
+
logger.info(`Requested limit: ${run.requestedLimit}`);
|
|
4058
|
+
if (run.quotaPerSource) {
|
|
4059
|
+
logger.info(`Quota per source: ${run.quotaPerSource}`);
|
|
4060
|
+
}
|
|
4061
|
+
console.group("Input Distribution:");
|
|
4062
|
+
for (const src of run.sourceSummaries) {
|
|
4063
|
+
const name = src.sourceName || src.sourceId;
|
|
4064
|
+
logger.info(`${name}:`);
|
|
4065
|
+
logger.info(` Provided: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`);
|
|
4066
|
+
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}]`);
|
|
4067
|
+
}
|
|
4068
|
+
console.groupEnd();
|
|
4069
|
+
console.group("Selection Results:");
|
|
4070
|
+
for (const breakdown of run.sourceBreakdowns) {
|
|
4071
|
+
const name = breakdown.sourceName || breakdown.sourceId;
|
|
4072
|
+
logger.info(`${name}:`);
|
|
4073
|
+
logger.info(
|
|
4074
|
+
` Selected: ${breakdown.totalSelected}/${breakdown.reviewsProvided + breakdown.newProvided} (${breakdown.selectionRate.toFixed(1)}%)`
|
|
4075
|
+
);
|
|
4076
|
+
logger.info(` Reviews: ${breakdown.reviewsSelected}/${breakdown.reviewsProvided}`);
|
|
4077
|
+
logger.info(` New: ${breakdown.newSelected}/${breakdown.newProvided}`);
|
|
4078
|
+
if (breakdown.reviewsProvided > 0 && breakdown.reviewsSelected === 0) {
|
|
4079
|
+
logger.info(` \u26A0\uFE0F Had reviews but none selected!`);
|
|
4080
|
+
}
|
|
4081
|
+
if (breakdown.totalSelected === 0 && breakdown.reviewsProvided + breakdown.newProvided > 0) {
|
|
4082
|
+
logger.info(` \u26A0\uFE0F Had cards but none selected!`);
|
|
4083
|
+
}
|
|
4084
|
+
}
|
|
4085
|
+
console.groupEnd();
|
|
4086
|
+
const selectionRates = run.sourceBreakdowns.map((b) => b.selectionRate);
|
|
4087
|
+
const avgRate = selectionRates.reduce((a, b) => a + b, 0) / selectionRates.length;
|
|
4088
|
+
const maxDeviation = Math.max(...selectionRates.map((r) => Math.abs(r - avgRate)));
|
|
4089
|
+
if (maxDeviation > 20) {
|
|
4090
|
+
logger.info(`
|
|
4091
|
+
\u26A0\uFE0F Significant imbalance detected (max deviation: ${maxDeviation.toFixed(1)}%)`);
|
|
4092
|
+
logger.info("Possible causes:");
|
|
4093
|
+
logger.info(" - Score range differences between sources");
|
|
4094
|
+
logger.info(" - One source has much better quality cards");
|
|
4095
|
+
logger.info(" - Different card availability (reviews vs new)");
|
|
4096
|
+
}
|
|
4097
|
+
console.groupEnd();
|
|
4098
|
+
},
|
|
4099
|
+
/**
|
|
4100
|
+
* Compare score distributions across sources.
|
|
4101
|
+
*/
|
|
4102
|
+
compareScores() {
|
|
4103
|
+
if (runHistory2.length === 0) {
|
|
4104
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4105
|
+
return;
|
|
4106
|
+
}
|
|
4107
|
+
const run = runHistory2[0];
|
|
4108
|
+
console.group("\u{1F4CA} Score Distribution Comparison");
|
|
4109
|
+
console.table(
|
|
4110
|
+
run.sourceSummaries.map((src) => ({
|
|
4111
|
+
source: src.sourceName || src.sourceId,
|
|
4112
|
+
cards: src.totalCards,
|
|
4113
|
+
min: src.bottomScore.toFixed(3),
|
|
4114
|
+
max: src.topScore.toFixed(3),
|
|
4115
|
+
avg: src.avgScore.toFixed(3),
|
|
4116
|
+
range: (src.topScore - src.bottomScore).toFixed(3)
|
|
4117
|
+
}))
|
|
4118
|
+
);
|
|
4119
|
+
const ranges = run.sourceSummaries.map((s) => s.topScore - s.bottomScore);
|
|
4120
|
+
const avgScores = run.sourceSummaries.map((s) => s.avgScore);
|
|
4121
|
+
const rangeDiff = Math.max(...ranges) - Math.min(...ranges);
|
|
4122
|
+
const avgDiff = Math.max(...avgScores) - Math.min(...avgScores);
|
|
4123
|
+
if (rangeDiff > 0.3 || avgDiff > 0.2) {
|
|
4124
|
+
logger.info("\n\u26A0\uFE0F Significant score distribution differences detected");
|
|
4125
|
+
logger.info(
|
|
4126
|
+
"This may cause one source to dominate selection if using global sorting (not quota-based)"
|
|
4127
|
+
);
|
|
4128
|
+
}
|
|
4129
|
+
console.groupEnd();
|
|
4130
|
+
},
|
|
4131
|
+
/**
|
|
4132
|
+
* Show detailed information for a specific card.
|
|
4133
|
+
*/
|
|
4134
|
+
showCard(cardId) {
|
|
4135
|
+
for (const run of runHistory2) {
|
|
4136
|
+
const card = run.cards.find((c) => c.cardId === cardId);
|
|
4137
|
+
if (card) {
|
|
4138
|
+
const source = run.sourceSummaries.find((s) => s.sourceIndex === card.sourceIndex);
|
|
4139
|
+
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
4140
|
+
logger.info(`Course: ${card.courseId}`);
|
|
4141
|
+
logger.info(`Source: ${source?.sourceName || source?.sourceId || "unknown"}`);
|
|
4142
|
+
logger.info(`Origin: ${card.origin}`);
|
|
4143
|
+
logger.info(`Score: ${card.score.toFixed(3)}`);
|
|
4144
|
+
if (card.rankInSource) {
|
|
4145
|
+
logger.info(`Rank in source: #${card.rankInSource}`);
|
|
4146
|
+
}
|
|
4147
|
+
if (card.rankInMix) {
|
|
4148
|
+
logger.info(`Rank in mixed results: #${card.rankInMix}`);
|
|
4149
|
+
}
|
|
4150
|
+
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
4151
|
+
if (!card.selected && card.rankInSource) {
|
|
4152
|
+
logger.info("\nWhy not selected:");
|
|
4153
|
+
if (run.quotaPerSource && card.rankInSource > run.quotaPerSource) {
|
|
4154
|
+
logger.info(` - Ranked #${card.rankInSource} in source, but quota was ${run.quotaPerSource}`);
|
|
4155
|
+
}
|
|
4156
|
+
logger.info(" - Check score compared to selected cards using .showRun()");
|
|
4157
|
+
}
|
|
4158
|
+
console.groupEnd();
|
|
4159
|
+
return;
|
|
4160
|
+
}
|
|
4161
|
+
}
|
|
4162
|
+
logger.info(`[Mixer Debug] Card '${cardId}' not found in recent runs.`);
|
|
4163
|
+
},
|
|
4164
|
+
/**
|
|
4165
|
+
* Show all runs in compact format.
|
|
4166
|
+
*/
|
|
4167
|
+
listRuns() {
|
|
4168
|
+
if (runHistory2.length === 0) {
|
|
4169
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4170
|
+
return;
|
|
4171
|
+
}
|
|
4172
|
+
console.table(
|
|
4173
|
+
runHistory2.map((r) => ({
|
|
4174
|
+
id: r.runId.slice(-8),
|
|
4175
|
+
time: r.timestamp.toLocaleTimeString(),
|
|
4176
|
+
mixer: r.mixerType,
|
|
4177
|
+
sources: r.sourceSummaries.length,
|
|
4178
|
+
selected: r.finalCount,
|
|
4179
|
+
reviews: r.reviewsSelected,
|
|
4180
|
+
new: r.newSelected
|
|
4181
|
+
}))
|
|
4182
|
+
);
|
|
4183
|
+
},
|
|
4184
|
+
/**
|
|
4185
|
+
* Export run history as JSON for bug reports.
|
|
4186
|
+
*/
|
|
4187
|
+
export() {
|
|
4188
|
+
const json = JSON.stringify(runHistory2, null, 2);
|
|
4189
|
+
logger.info("[Mixer Debug] Run history exported. Copy the returned string or use:");
|
|
4190
|
+
logger.info(" copy(window.skuilder.mixer.export())");
|
|
4191
|
+
return json;
|
|
4192
|
+
},
|
|
4193
|
+
/**
|
|
4194
|
+
* Clear run history.
|
|
4195
|
+
*/
|
|
4196
|
+
clear() {
|
|
4197
|
+
runHistory2.length = 0;
|
|
4198
|
+
logger.info("[Mixer Debug] Run history cleared.");
|
|
4199
|
+
},
|
|
4200
|
+
/**
|
|
4201
|
+
* Show help.
|
|
2291
4202
|
*/
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
4203
|
+
help() {
|
|
4204
|
+
logger.info(`
|
|
4205
|
+
\u{1F3A8} Mixer Debug API
|
|
4206
|
+
|
|
4207
|
+
Commands:
|
|
4208
|
+
.showLastMix() Show summary of most recent mixer run
|
|
4209
|
+
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
4210
|
+
.explainSourceBalance() Analyze source balance and selection patterns
|
|
4211
|
+
.compareScores() Compare score distributions across sources
|
|
4212
|
+
.showCard(cardId) Show mixer decisions for a specific card
|
|
4213
|
+
.listRuns() List all captured runs in table format
|
|
4214
|
+
.export() Export run history as JSON for bug reports
|
|
4215
|
+
.clear() Clear run history
|
|
4216
|
+
.runs Access raw run history array
|
|
4217
|
+
.help() Show this help message
|
|
4218
|
+
|
|
4219
|
+
Example:
|
|
4220
|
+
window.skuilder.mixer.showLastMix()
|
|
4221
|
+
window.skuilder.mixer.explainSourceBalance()
|
|
4222
|
+
window.skuilder.mixer.compareScores()
|
|
4223
|
+
`);
|
|
2296
4224
|
}
|
|
2297
4225
|
};
|
|
4226
|
+
mountMixerDebugger();
|
|
2298
4227
|
}
|
|
2299
4228
|
});
|
|
2300
4229
|
|
|
2301
|
-
// src/
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
4230
|
+
// src/study/SessionDebugger.ts
|
|
4231
|
+
function showCurrentQueue() {
|
|
4232
|
+
if (!activeSession) {
|
|
4233
|
+
logger.info("[Session Debug] No active session.");
|
|
4234
|
+
return;
|
|
2306
4235
|
}
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
USER_GOAL_NAVIGATOR_STUB: () => USER_GOAL_NAVIGATOR_STUB
|
|
2313
|
-
});
|
|
2314
|
-
var USER_GOAL_NAVIGATOR_STUB;
|
|
2315
|
-
var init_userGoalStub = __esm({
|
|
2316
|
-
"src/core/navigators/filters/userGoalStub.ts"() {
|
|
2317
|
-
"use strict";
|
|
2318
|
-
USER_GOAL_NAVIGATOR_STUB = true;
|
|
4236
|
+
const latest = activeSession.queueSnapshots[activeSession.queueSnapshots.length - 1] || activeSession.initialQueues;
|
|
4237
|
+
console.group("\u{1F4CA} Current Queue State");
|
|
4238
|
+
logger.info(`Review Queue: ${latest.reviewQLength} cards`);
|
|
4239
|
+
if (latest.reviewQNext3 && latest.reviewQNext3.length > 0) {
|
|
4240
|
+
logger.info(` Next: ${latest.reviewQNext3.join(", ")}`);
|
|
2319
4241
|
}
|
|
2320
|
-
});
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
var globImport_filters;
|
|
2324
|
-
var init_2 = __esm({
|
|
2325
|
-
'import("./filters/**/*") in src/core/navigators/index.ts'() {
|
|
2326
|
-
globImport_filters = __glob({
|
|
2327
|
-
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
2328
|
-
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
2329
|
-
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
2330
|
-
"./filters/index.ts": () => Promise.resolve().then(() => (init_filters(), filters_exports)),
|
|
2331
|
-
"./filters/inferredPreferenceStub.ts": () => Promise.resolve().then(() => (init_inferredPreferenceStub(), inferredPreferenceStub_exports)),
|
|
2332
|
-
"./filters/interferenceMitigator.ts": () => Promise.resolve().then(() => (init_interferenceMitigator(), interferenceMitigator_exports)),
|
|
2333
|
-
"./filters/relativePriority.ts": () => Promise.resolve().then(() => (init_relativePriority(), relativePriority_exports)),
|
|
2334
|
-
"./filters/types.ts": () => Promise.resolve().then(() => (init_types2(), types_exports2)),
|
|
2335
|
-
"./filters/userGoalStub.ts": () => Promise.resolve().then(() => (init_userGoalStub(), userGoalStub_exports)),
|
|
2336
|
-
"./filters/userTagPreference.ts": () => Promise.resolve().then(() => (init_userTagPreference(), userTagPreference_exports))
|
|
2337
|
-
});
|
|
4242
|
+
logger.info(`New Queue: ${latest.newQLength} cards`);
|
|
4243
|
+
if (latest.newQNext3 && latest.newQNext3.length > 0) {
|
|
4244
|
+
logger.info(` Next: ${latest.newQNext3.join(", ")}`);
|
|
2338
4245
|
}
|
|
2339
|
-
});
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
4246
|
+
logger.info(`Failed Queue: ${latest.failedQLength} cards`);
|
|
4247
|
+
console.groupEnd();
|
|
4248
|
+
}
|
|
4249
|
+
function showPresentationHistory(sessionIndex = 0) {
|
|
4250
|
+
const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
|
|
4251
|
+
if (!session) {
|
|
4252
|
+
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
4253
|
+
return;
|
|
2346
4254
|
}
|
|
2347
|
-
});
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
"src/core/orchestration/learning.ts"() {
|
|
2352
|
-
"use strict";
|
|
2353
|
-
init_contentNavigationStrategy();
|
|
2354
|
-
init_types_legacy();
|
|
2355
|
-
init_logger();
|
|
4255
|
+
console.group(`\u{1F4DC} Session History: ${session.sessionId}`);
|
|
4256
|
+
logger.info(`Started: ${session.startTime.toLocaleTimeString()}`);
|
|
4257
|
+
if (session.endTime) {
|
|
4258
|
+
logger.info(`Ended: ${session.endTime.toLocaleTimeString()}`);
|
|
2356
4259
|
}
|
|
2357
|
-
});
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
4260
|
+
logger.info(`Cards presented: ${session.presentations.length}`);
|
|
4261
|
+
if (session.presentations.length > 0) {
|
|
4262
|
+
console.table(
|
|
4263
|
+
session.presentations.map((p) => ({
|
|
4264
|
+
"#": p.sequenceNumber,
|
|
4265
|
+
course: p.courseName || p.courseId.slice(0, 8),
|
|
4266
|
+
origin: p.origin,
|
|
4267
|
+
queue: p.queueSource,
|
|
4268
|
+
score: p.score?.toFixed(3) || "-",
|
|
4269
|
+
time: p.timestamp.toLocaleTimeString()
|
|
4270
|
+
}))
|
|
4271
|
+
);
|
|
2363
4272
|
}
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
init_types_legacy();
|
|
2372
|
-
init_logger();
|
|
4273
|
+
console.groupEnd();
|
|
4274
|
+
}
|
|
4275
|
+
function showInterleaving(sessionIndex = 0) {
|
|
4276
|
+
const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
|
|
4277
|
+
if (!session) {
|
|
4278
|
+
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
4279
|
+
return;
|
|
2373
4280
|
}
|
|
2374
|
-
});
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
4281
|
+
console.group("\u{1F500} Interleaving Analysis");
|
|
4282
|
+
const courseCounts = /* @__PURE__ */ new Map();
|
|
4283
|
+
const courseOrigins = /* @__PURE__ */ new Map();
|
|
4284
|
+
session.presentations.forEach((p) => {
|
|
4285
|
+
const name = p.courseName || p.courseId;
|
|
4286
|
+
courseCounts.set(name, (courseCounts.get(name) || 0) + 1);
|
|
4287
|
+
if (!courseOrigins.has(name)) {
|
|
4288
|
+
courseOrigins.set(name, { review: 0, new: 0, failed: 0 });
|
|
4289
|
+
}
|
|
4290
|
+
const origins = courseOrigins.get(name);
|
|
4291
|
+
origins[p.origin]++;
|
|
4292
|
+
});
|
|
4293
|
+
logger.info("Course distribution:");
|
|
4294
|
+
console.table(
|
|
4295
|
+
Array.from(courseCounts.entries()).map(([course, count]) => {
|
|
4296
|
+
const origins = courseOrigins.get(course);
|
|
4297
|
+
return {
|
|
4298
|
+
course,
|
|
4299
|
+
total: count,
|
|
4300
|
+
reviews: origins.review,
|
|
4301
|
+
new: origins.new,
|
|
4302
|
+
failed: origins.failed,
|
|
4303
|
+
percentage: (count / session.presentations.length * 100).toFixed(1) + "%"
|
|
4304
|
+
};
|
|
4305
|
+
})
|
|
4306
|
+
);
|
|
4307
|
+
if (session.presentations.length > 0) {
|
|
4308
|
+
logger.info("\nPresentation sequence (first 20):");
|
|
4309
|
+
const sequence = session.presentations.slice(0, 20).map((p, idx) => `${idx + 1}. ${p.courseName || p.courseId.slice(0, 8)} (${p.origin})`).join("\n");
|
|
4310
|
+
logger.info(sequence);
|
|
2382
4311
|
}
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
4312
|
+
let maxCluster = 0;
|
|
4313
|
+
let currentCluster = 1;
|
|
4314
|
+
let currentCourse = session.presentations[0]?.courseId;
|
|
4315
|
+
for (let i = 1; i < session.presentations.length; i++) {
|
|
4316
|
+
if (session.presentations[i].courseId === currentCourse) {
|
|
4317
|
+
currentCluster++;
|
|
4318
|
+
maxCluster = Math.max(maxCluster, currentCluster);
|
|
4319
|
+
} else {
|
|
4320
|
+
currentCourse = session.presentations[i].courseId;
|
|
4321
|
+
currentCluster = 1;
|
|
4322
|
+
}
|
|
4323
|
+
}
|
|
4324
|
+
if (maxCluster > 3) {
|
|
4325
|
+
logger.info(`
|
|
4326
|
+
\u26A0\uFE0F Detected clustering: max ${maxCluster} cards from same course in a row`);
|
|
4327
|
+
logger.info("This suggests cards are sorted by score rather than round-robin by course.");
|
|
4328
|
+
}
|
|
4329
|
+
console.groupEnd();
|
|
2394
4330
|
}
|
|
2395
|
-
function
|
|
2396
|
-
|
|
2397
|
-
const
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
|
|
4331
|
+
function mountSessionDebugger() {
|
|
4332
|
+
if (typeof window === "undefined") return;
|
|
4333
|
+
const win = window;
|
|
4334
|
+
win.skuilder = win.skuilder || {};
|
|
4335
|
+
win.skuilder.session = sessionDebugAPI;
|
|
2401
4336
|
}
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
4337
|
+
var activeSession, sessionHistory, sessionDebugAPI;
|
|
4338
|
+
var init_SessionDebugger = __esm({
|
|
4339
|
+
"src/study/SessionDebugger.ts"() {
|
|
4340
|
+
"use strict";
|
|
4341
|
+
init_logger();
|
|
4342
|
+
activeSession = null;
|
|
4343
|
+
sessionHistory = [];
|
|
4344
|
+
sessionDebugAPI = {
|
|
4345
|
+
/**
|
|
4346
|
+
* Get raw session history for programmatic access.
|
|
4347
|
+
*/
|
|
4348
|
+
get sessions() {
|
|
4349
|
+
return [...sessionHistory];
|
|
4350
|
+
},
|
|
4351
|
+
/**
|
|
4352
|
+
* Get active session if any.
|
|
4353
|
+
*/
|
|
4354
|
+
get active() {
|
|
4355
|
+
return activeSession;
|
|
4356
|
+
},
|
|
4357
|
+
/**
|
|
4358
|
+
* Show current queue state.
|
|
4359
|
+
*/
|
|
4360
|
+
showQueue() {
|
|
4361
|
+
showCurrentQueue();
|
|
4362
|
+
},
|
|
4363
|
+
/**
|
|
4364
|
+
* Show presentation history for current or past session.
|
|
4365
|
+
*/
|
|
4366
|
+
showHistory(sessionIndex = 0) {
|
|
4367
|
+
showPresentationHistory(sessionIndex);
|
|
4368
|
+
},
|
|
4369
|
+
/**
|
|
4370
|
+
* Analyze course interleaving pattern.
|
|
4371
|
+
*/
|
|
4372
|
+
showInterleaving(sessionIndex = 0) {
|
|
4373
|
+
showInterleaving(sessionIndex);
|
|
4374
|
+
},
|
|
4375
|
+
/**
|
|
4376
|
+
* List all tracked sessions.
|
|
4377
|
+
*/
|
|
4378
|
+
listSessions() {
|
|
4379
|
+
if (activeSession) {
|
|
4380
|
+
logger.info(`Active session: ${activeSession.sessionId} (${activeSession.presentations.length} cards presented)`);
|
|
4381
|
+
}
|
|
4382
|
+
if (sessionHistory.length === 0) {
|
|
4383
|
+
logger.info("[Session Debug] No completed sessions in history.");
|
|
4384
|
+
return;
|
|
4385
|
+
}
|
|
4386
|
+
console.table(
|
|
4387
|
+
sessionHistory.map((s, idx) => ({
|
|
4388
|
+
index: idx,
|
|
4389
|
+
id: s.sessionId.slice(-8),
|
|
4390
|
+
started: s.startTime.toLocaleTimeString(),
|
|
4391
|
+
ended: s.endTime?.toLocaleTimeString() || "incomplete",
|
|
4392
|
+
cards: s.presentations.length
|
|
4393
|
+
}))
|
|
4394
|
+
);
|
|
4395
|
+
},
|
|
4396
|
+
/**
|
|
4397
|
+
* Export session history as JSON for bug reports.
|
|
4398
|
+
*/
|
|
4399
|
+
export() {
|
|
4400
|
+
const data = {
|
|
4401
|
+
active: activeSession,
|
|
4402
|
+
history: sessionHistory
|
|
4403
|
+
};
|
|
4404
|
+
const json = JSON.stringify(data, null, 2);
|
|
4405
|
+
logger.info("[Session Debug] Session data exported. Copy the returned string or use:");
|
|
4406
|
+
logger.info(" copy(window.skuilder.session.export())");
|
|
4407
|
+
return json;
|
|
4408
|
+
},
|
|
4409
|
+
/**
|
|
4410
|
+
* Clear session history.
|
|
4411
|
+
*/
|
|
4412
|
+
clear() {
|
|
4413
|
+
sessionHistory.length = 0;
|
|
4414
|
+
logger.info("[Session Debug] Session history cleared.");
|
|
4415
|
+
},
|
|
4416
|
+
/**
|
|
4417
|
+
* Show help.
|
|
4418
|
+
*/
|
|
4419
|
+
help() {
|
|
4420
|
+
logger.info(`
|
|
4421
|
+
\u{1F3AF} Session Debug API
|
|
4422
|
+
|
|
4423
|
+
Commands:
|
|
4424
|
+
.showQueue() Show current queue state (active session only)
|
|
4425
|
+
.showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
|
|
4426
|
+
.showInterleaving(index?) Analyze course interleaving pattern
|
|
4427
|
+
.listSessions() List all tracked sessions
|
|
4428
|
+
.export() Export session data as JSON for bug reports
|
|
4429
|
+
.clear() Clear session history
|
|
4430
|
+
.sessions Access raw session history array
|
|
4431
|
+
.active Access active session (if any)
|
|
4432
|
+
.help() Show this help message
|
|
4433
|
+
|
|
4434
|
+
Example:
|
|
4435
|
+
window.skuilder.session.showHistory()
|
|
4436
|
+
window.skuilder.session.showInterleaving()
|
|
4437
|
+
window.skuilder.session.showQueue()
|
|
4438
|
+
`);
|
|
4439
|
+
}
|
|
2419
4440
|
};
|
|
4441
|
+
mountSessionDebugger();
|
|
2420
4442
|
}
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
userId,
|
|
2427
|
-
courseConfig,
|
|
2428
|
-
getEffectiveWeight(strategyId, learnable) {
|
|
2429
|
-
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
2430
|
-
},
|
|
2431
|
-
getDeviation(strategyId) {
|
|
2432
|
-
return computeDeviation(userId, strategyId, salt);
|
|
2433
|
-
}
|
|
2434
|
-
};
|
|
2435
|
-
}
|
|
2436
|
-
var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
|
|
2437
|
-
var init_orchestration = __esm({
|
|
2438
|
-
"src/core/orchestration/index.ts"() {
|
|
4443
|
+
});
|
|
4444
|
+
|
|
4445
|
+
// src/study/SessionController.ts
|
|
4446
|
+
var init_SessionController = __esm({
|
|
4447
|
+
"src/study/SessionController.ts"() {
|
|
2439
4448
|
"use strict";
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
4449
|
+
init_SrsService();
|
|
4450
|
+
init_EloService();
|
|
4451
|
+
init_ResponseProcessor();
|
|
4452
|
+
init_CardHydrationService();
|
|
4453
|
+
init_ItemQueue();
|
|
4454
|
+
init_couch();
|
|
2444
4455
|
init_recording();
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
4456
|
+
init_util2();
|
|
4457
|
+
init_navigators();
|
|
4458
|
+
init_SourceMixer();
|
|
4459
|
+
init_MixerDebugger();
|
|
4460
|
+
init_SessionDebugger();
|
|
4461
|
+
init_logger();
|
|
2449
4462
|
}
|
|
2450
4463
|
});
|
|
2451
4464
|
|
|
@@ -2529,15 +4542,16 @@ function logCardProvenance(cards, maxCards = 3) {
|
|
|
2529
4542
|
}
|
|
2530
4543
|
}
|
|
2531
4544
|
}
|
|
2532
|
-
var
|
|
4545
|
+
var import_common12, VERBOSE_RESULTS, Pipeline;
|
|
2533
4546
|
var init_Pipeline = __esm({
|
|
2534
4547
|
"src/core/navigators/Pipeline.ts"() {
|
|
2535
4548
|
"use strict";
|
|
2536
|
-
|
|
4549
|
+
import_common12 = require("@vue-skuilder/common");
|
|
2537
4550
|
init_navigators();
|
|
2538
4551
|
init_logger();
|
|
2539
4552
|
init_orchestration();
|
|
2540
4553
|
init_PipelineDebugger();
|
|
4554
|
+
init_SessionController();
|
|
2541
4555
|
VERBOSE_RESULTS = true;
|
|
2542
4556
|
Pipeline = class extends ContentNavigator {
|
|
2543
4557
|
generator;
|
|
@@ -2697,8 +4711,9 @@ var init_Pipeline = __esm({
|
|
|
2697
4711
|
generatorSummaries,
|
|
2698
4712
|
generatedCount,
|
|
2699
4713
|
filterImpacts,
|
|
2700
|
-
|
|
2701
|
-
result
|
|
4714
|
+
cards,
|
|
4715
|
+
result,
|
|
4716
|
+
context.userElo
|
|
2702
4717
|
);
|
|
2703
4718
|
captureRun(report);
|
|
2704
4719
|
} catch (e) {
|
|
@@ -2781,7 +4796,7 @@ var init_Pipeline = __esm({
|
|
|
2781
4796
|
card.provenance.push({
|
|
2782
4797
|
strategy: "ephemeralHint",
|
|
2783
4798
|
strategyId: "ephemeral-hint",
|
|
2784
|
-
strategyName: "Replan Hint",
|
|
4799
|
+
strategyName: hints._label ? `Replan Hint (${hints._label})` : "Replan Hint",
|
|
2785
4800
|
action: "boosted",
|
|
2786
4801
|
score: card.score,
|
|
2787
4802
|
reason: `boostTag ${pattern} \xD7${factor}`
|
|
@@ -2798,7 +4813,7 @@ var init_Pipeline = __esm({
|
|
|
2798
4813
|
card.provenance.push({
|
|
2799
4814
|
strategy: "ephemeralHint",
|
|
2800
4815
|
strategyId: "ephemeral-hint",
|
|
2801
|
-
strategyName: "Replan Hint",
|
|
4816
|
+
strategyName: hints._label ? `Replan Hint (${hints._label})` : "Replan Hint",
|
|
2802
4817
|
action: "boosted",
|
|
2803
4818
|
score: card.score,
|
|
2804
4819
|
reason: `boostCard ${pattern} \xD7${factor}`
|
|
@@ -2808,6 +4823,7 @@ var init_Pipeline = __esm({
|
|
|
2808
4823
|
}
|
|
2809
4824
|
}
|
|
2810
4825
|
const cardIds = new Set(cards.map((c) => c.cardId));
|
|
4826
|
+
const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
|
|
2811
4827
|
const inject = (card, reason) => {
|
|
2812
4828
|
if (!cardIds.has(card.cardId)) {
|
|
2813
4829
|
const floorScore = Math.max(card.score, 1);
|
|
@@ -2819,7 +4835,7 @@ var init_Pipeline = __esm({
|
|
|
2819
4835
|
{
|
|
2820
4836
|
strategy: "ephemeralHint",
|
|
2821
4837
|
strategyId: "ephemeral-hint",
|
|
2822
|
-
strategyName:
|
|
4838
|
+
strategyName: hintLabel,
|
|
2823
4839
|
action: "boosted",
|
|
2824
4840
|
score: floorScore,
|
|
2825
4841
|
reason
|
|
@@ -2858,7 +4874,7 @@ var init_Pipeline = __esm({
|
|
|
2858
4874
|
let userElo = 1e3;
|
|
2859
4875
|
try {
|
|
2860
4876
|
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
2861
|
-
const courseElo = (0,
|
|
4877
|
+
const courseElo = (0, import_common12.toCourseElo)(courseReg.elo);
|
|
2862
4878
|
userElo = courseElo.global.score;
|
|
2863
4879
|
} catch (e) {
|
|
2864
4880
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
@@ -2911,6 +4927,34 @@ var init_Pipeline = __esm({
|
|
|
2911
4927
|
return [...new Set(ids)];
|
|
2912
4928
|
}
|
|
2913
4929
|
// ---------------------------------------------------------------------------
|
|
4930
|
+
// Tag ELO diagnostic
|
|
4931
|
+
// ---------------------------------------------------------------------------
|
|
4932
|
+
/**
|
|
4933
|
+
* Get the user's per-tag ELO data for specified tags (or all tags).
|
|
4934
|
+
* Useful for diagnosing why hierarchy gates are open/closed.
|
|
4935
|
+
*/
|
|
4936
|
+
async getTagEloStatus(tagFilter) {
|
|
4937
|
+
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
4938
|
+
const courseElo = (0, import_common12.toCourseElo)(courseReg.elo);
|
|
4939
|
+
const result = {};
|
|
4940
|
+
if (!tagFilter) {
|
|
4941
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
4942
|
+
result[tag] = { score: data.score, count: data.count };
|
|
4943
|
+
}
|
|
4944
|
+
} else {
|
|
4945
|
+
const patterns = Array.isArray(tagFilter) ? tagFilter : [tagFilter];
|
|
4946
|
+
for (const pattern of patterns) {
|
|
4947
|
+
const regex = globToRegex(pattern);
|
|
4948
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
4949
|
+
if (regex.test(tag)) {
|
|
4950
|
+
result[tag] = { score: data.score, count: data.count };
|
|
4951
|
+
}
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
}
|
|
4955
|
+
return result;
|
|
4956
|
+
}
|
|
4957
|
+
// ---------------------------------------------------------------------------
|
|
2914
4958
|
// Card-space diagnostic
|
|
2915
4959
|
// ---------------------------------------------------------------------------
|
|
2916
4960
|
/**
|
|
@@ -3489,11 +5533,11 @@ var init_navigators = __esm({
|
|
|
3489
5533
|
});
|
|
3490
5534
|
|
|
3491
5535
|
// src/impl/couch/courseDB.ts
|
|
3492
|
-
var
|
|
5536
|
+
var import_common13;
|
|
3493
5537
|
var init_courseDB = __esm({
|
|
3494
5538
|
"src/impl/couch/courseDB.ts"() {
|
|
3495
5539
|
"use strict";
|
|
3496
|
-
|
|
5540
|
+
import_common13 = require("@vue-skuilder/common");
|
|
3497
5541
|
init_couch();
|
|
3498
5542
|
init_updateQueue();
|
|
3499
5543
|
init_types_legacy();
|
|
@@ -3508,13 +5552,13 @@ var init_courseDB = __esm({
|
|
|
3508
5552
|
});
|
|
3509
5553
|
|
|
3510
5554
|
// src/impl/couch/classroomDB.ts
|
|
3511
|
-
var
|
|
5555
|
+
var import_moment6;
|
|
3512
5556
|
var init_classroomDB2 = __esm({
|
|
3513
5557
|
"src/impl/couch/classroomDB.ts"() {
|
|
3514
5558
|
"use strict";
|
|
3515
5559
|
init_factory();
|
|
3516
5560
|
init_logger();
|
|
3517
|
-
|
|
5561
|
+
import_moment6 = __toESM(require("moment"), 1);
|
|
3518
5562
|
init_pouchdb_setup();
|
|
3519
5563
|
init_couch();
|
|
3520
5564
|
init_courseDB();
|
|
@@ -3556,14 +5600,14 @@ var init_auth = __esm({
|
|
|
3556
5600
|
});
|
|
3557
5601
|
|
|
3558
5602
|
// src/impl/couch/CouchDBSyncStrategy.ts
|
|
3559
|
-
var
|
|
5603
|
+
var import_common14;
|
|
3560
5604
|
var init_CouchDBSyncStrategy = __esm({
|
|
3561
5605
|
"src/impl/couch/CouchDBSyncStrategy.ts"() {
|
|
3562
5606
|
"use strict";
|
|
3563
5607
|
init_factory();
|
|
3564
5608
|
init_types_legacy();
|
|
3565
5609
|
init_logger();
|
|
3566
|
-
|
|
5610
|
+
import_common14 = require("@vue-skuilder/common");
|
|
3567
5611
|
init_common();
|
|
3568
5612
|
init_pouchdb_setup();
|
|
3569
5613
|
init_couch();
|
|
@@ -3591,14 +5635,14 @@ function createPouchDBConfig() {
|
|
|
3591
5635
|
}
|
|
3592
5636
|
return pouchDBincludeCredentialsConfig;
|
|
3593
5637
|
}
|
|
3594
|
-
var import_cross_fetch2,
|
|
5638
|
+
var import_cross_fetch2, import_moment7, import_process, isBrowser, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig;
|
|
3595
5639
|
var init_couch = __esm({
|
|
3596
5640
|
"src/impl/couch/index.ts"() {
|
|
3597
5641
|
"use strict";
|
|
3598
5642
|
init_factory();
|
|
3599
5643
|
init_types_legacy();
|
|
3600
5644
|
import_cross_fetch2 = __toESM(require("cross-fetch"), 1);
|
|
3601
|
-
|
|
5645
|
+
import_moment7 = __toESM(require("moment"), 1);
|
|
3602
5646
|
init_logger();
|
|
3603
5647
|
init_pouchdb_setup();
|
|
3604
5648
|
import_process = __toESM(require("process"), 1);
|
|
@@ -3798,14 +5842,14 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
3798
5842
|
async function getUserClassrooms(user) {
|
|
3799
5843
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
3800
5844
|
}
|
|
3801
|
-
var
|
|
5845
|
+
var import_common16, import_moment8, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
3802
5846
|
var init_BaseUserDB = __esm({
|
|
3803
5847
|
"src/impl/common/BaseUserDB.ts"() {
|
|
3804
5848
|
"use strict";
|
|
3805
5849
|
init_core();
|
|
3806
5850
|
init_util();
|
|
3807
|
-
|
|
3808
|
-
|
|
5851
|
+
import_common16 = require("@vue-skuilder/common");
|
|
5852
|
+
import_moment8 = __toESM(require("moment"), 1);
|
|
3809
5853
|
init_types_legacy();
|
|
3810
5854
|
init_logger();
|
|
3811
5855
|
init_userDBHelpers();
|
|
@@ -3854,7 +5898,7 @@ Currently logged-in as ${this._username}.`
|
|
|
3854
5898
|
);
|
|
3855
5899
|
}
|
|
3856
5900
|
const result = await this.syncStrategy.createAccount(username, password);
|
|
3857
|
-
if (result.status ===
|
|
5901
|
+
if (result.status === import_common16.Status.ok) {
|
|
3858
5902
|
log3(`Account created successfully, updating username to ${username}`);
|
|
3859
5903
|
this._username = username;
|
|
3860
5904
|
try {
|
|
@@ -3896,7 +5940,7 @@ Currently logged-in as ${this._username}.`
|
|
|
3896
5940
|
async resetUserData() {
|
|
3897
5941
|
if (this.syncStrategy.canAuthenticate()) {
|
|
3898
5942
|
return {
|
|
3899
|
-
status:
|
|
5943
|
+
status: import_common16.Status.error,
|
|
3900
5944
|
error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
|
|
3901
5945
|
};
|
|
3902
5946
|
}
|
|
@@ -3918,11 +5962,11 @@ Currently logged-in as ${this._username}.`
|
|
|
3918
5962
|
await localDB.bulkDocs(docsToDelete);
|
|
3919
5963
|
}
|
|
3920
5964
|
await this.init();
|
|
3921
|
-
return { status:
|
|
5965
|
+
return { status: import_common16.Status.ok };
|
|
3922
5966
|
} catch (error) {
|
|
3923
5967
|
logger.error("Failed to reset user data:", error);
|
|
3924
5968
|
return {
|
|
3925
|
-
status:
|
|
5969
|
+
status: import_common16.Status.error,
|
|
3926
5970
|
error: error instanceof Error ? error.message : "Unknown error during reset"
|
|
3927
5971
|
};
|
|
3928
5972
|
}
|
|
@@ -4069,7 +6113,7 @@ Currently logged-in as ${this._username}.`
|
|
|
4069
6113
|
);
|
|
4070
6114
|
return reviews.rows.filter((r) => {
|
|
4071
6115
|
if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
|
|
4072
|
-
const date =
|
|
6116
|
+
const date = import_moment8.default.utc(
|
|
4073
6117
|
r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
|
|
4074
6118
|
REVIEW_TIME_FORMAT
|
|
4075
6119
|
);
|
|
@@ -4082,11 +6126,11 @@ Currently logged-in as ${this._username}.`
|
|
|
4082
6126
|
}).map((r) => r.doc);
|
|
4083
6127
|
}
|
|
4084
6128
|
async getReviewsForcast(daysCount) {
|
|
4085
|
-
const time =
|
|
6129
|
+
const time = import_moment8.default.utc().add(daysCount, "days");
|
|
4086
6130
|
return this.getReviewstoDate(time);
|
|
4087
6131
|
}
|
|
4088
6132
|
async getPendingReviews(course_id) {
|
|
4089
|
-
const now =
|
|
6133
|
+
const now = import_moment8.default.utc();
|
|
4090
6134
|
return this.getReviewstoDate(now, course_id);
|
|
4091
6135
|
}
|
|
4092
6136
|
async getScheduledReviewCount(course_id) {
|
|
@@ -4373,7 +6417,7 @@ Currently logged-in as ${this._username}.`
|
|
|
4373
6417
|
*/
|
|
4374
6418
|
async putCardRecord(record) {
|
|
4375
6419
|
const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
|
|
4376
|
-
record.timeStamp =
|
|
6420
|
+
record.timeStamp = import_moment8.default.utc(record.timeStamp).toString();
|
|
4377
6421
|
try {
|
|
4378
6422
|
const cardHistory = await this.update(
|
|
4379
6423
|
cardHistoryID,
|
|
@@ -4389,7 +6433,7 @@ Currently logged-in as ${this._username}.`
|
|
|
4389
6433
|
const ret = {
|
|
4390
6434
|
...record2
|
|
4391
6435
|
};
|
|
4392
|
-
ret.timeStamp =
|
|
6436
|
+
ret.timeStamp = import_moment8.default.utc(record2.timeStamp);
|
|
4393
6437
|
return ret;
|
|
4394
6438
|
});
|
|
4395
6439
|
return cardHistory;
|
|
@@ -4716,24 +6760,24 @@ var init_factory = __esm({
|
|
|
4716
6760
|
});
|
|
4717
6761
|
|
|
4718
6762
|
// src/study/TagFilteredContentSource.ts
|
|
4719
|
-
var
|
|
6763
|
+
var import_common18;
|
|
4720
6764
|
var init_TagFilteredContentSource = __esm({
|
|
4721
6765
|
"src/study/TagFilteredContentSource.ts"() {
|
|
4722
6766
|
"use strict";
|
|
4723
|
-
|
|
6767
|
+
import_common18 = require("@vue-skuilder/common");
|
|
4724
6768
|
init_courseDB();
|
|
4725
6769
|
init_logger();
|
|
4726
6770
|
}
|
|
4727
6771
|
});
|
|
4728
6772
|
|
|
4729
6773
|
// src/core/interfaces/contentSource.ts
|
|
4730
|
-
var
|
|
6774
|
+
var import_common19;
|
|
4731
6775
|
var init_contentSource = __esm({
|
|
4732
6776
|
"src/core/interfaces/contentSource.ts"() {
|
|
4733
6777
|
"use strict";
|
|
4734
6778
|
init_factory();
|
|
4735
6779
|
init_classroomDB2();
|
|
4736
|
-
|
|
6780
|
+
import_common19 = require("@vue-skuilder/common");
|
|
4737
6781
|
init_TagFilteredContentSource();
|
|
4738
6782
|
}
|
|
4739
6783
|
});
|
|
@@ -4797,17 +6841,17 @@ var init_userOutcome = __esm({
|
|
|
4797
6841
|
});
|
|
4798
6842
|
|
|
4799
6843
|
// src/core/bulkImport/cardProcessor.ts
|
|
4800
|
-
var
|
|
6844
|
+
var import_common20;
|
|
4801
6845
|
var init_cardProcessor = __esm({
|
|
4802
6846
|
"src/core/bulkImport/cardProcessor.ts"() {
|
|
4803
6847
|
"use strict";
|
|
4804
|
-
|
|
6848
|
+
import_common20 = require("@vue-skuilder/common");
|
|
4805
6849
|
init_logger();
|
|
4806
6850
|
}
|
|
4807
6851
|
});
|
|
4808
6852
|
|
|
4809
6853
|
// src/core/bulkImport/types.ts
|
|
4810
|
-
var
|
|
6854
|
+
var init_types5 = __esm({
|
|
4811
6855
|
"src/core/bulkImport/types.ts"() {
|
|
4812
6856
|
"use strict";
|
|
4813
6857
|
}
|
|
@@ -4818,7 +6862,7 @@ var init_bulkImport = __esm({
|
|
|
4818
6862
|
"src/core/bulkImport/index.ts"() {
|
|
4819
6863
|
"use strict";
|
|
4820
6864
|
init_cardProcessor();
|
|
4821
|
-
|
|
6865
|
+
init_types5();
|
|
4822
6866
|
}
|
|
4823
6867
|
});
|
|
4824
6868
|
|
|
@@ -5170,7 +7214,7 @@ var init_core = __esm({
|
|
|
5170
7214
|
});
|
|
5171
7215
|
|
|
5172
7216
|
// src/impl/static/StaticDataUnpacker.ts
|
|
5173
|
-
var pathUtils,
|
|
7217
|
+
var pathUtils, nodeFS3, StaticDataUnpacker;
|
|
5174
7218
|
var init_StaticDataUnpacker = __esm({
|
|
5175
7219
|
"src/impl/static/StaticDataUnpacker.ts"() {
|
|
5176
7220
|
"use strict";
|
|
@@ -5187,10 +7231,10 @@ var init_StaticDataUnpacker = __esm({
|
|
|
5187
7231
|
return false;
|
|
5188
7232
|
}
|
|
5189
7233
|
};
|
|
5190
|
-
|
|
7234
|
+
nodeFS3 = null;
|
|
5191
7235
|
try {
|
|
5192
7236
|
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
5193
|
-
|
|
7237
|
+
nodeFS3 = eval("require")("fs");
|
|
5194
7238
|
}
|
|
5195
7239
|
} catch {
|
|
5196
7240
|
}
|
|
@@ -5377,8 +7421,8 @@ var init_StaticDataUnpacker = __esm({
|
|
|
5377
7421
|
const chunkPath = `${this.basePath}/${chunk.path}`;
|
|
5378
7422
|
logger.debug(`Loading chunk from ${chunkPath}`);
|
|
5379
7423
|
let documents;
|
|
5380
|
-
if (this.isLocalPath(chunkPath) &&
|
|
5381
|
-
const fileContent = await
|
|
7424
|
+
if (this.isLocalPath(chunkPath) && nodeFS3) {
|
|
7425
|
+
const fileContent = await nodeFS3.promises.readFile(chunkPath, "utf8");
|
|
5382
7426
|
documents = JSON.parse(fileContent);
|
|
5383
7427
|
} else {
|
|
5384
7428
|
const response = await fetch(chunkPath);
|
|
@@ -5416,8 +7460,8 @@ var init_StaticDataUnpacker = __esm({
|
|
|
5416
7460
|
const indexPath = `${this.basePath}/${indexMeta.path}`;
|
|
5417
7461
|
logger.debug(`Loading index from ${indexPath}`);
|
|
5418
7462
|
let indexData;
|
|
5419
|
-
if (this.isLocalPath(indexPath) &&
|
|
5420
|
-
const fileContent = await
|
|
7463
|
+
if (this.isLocalPath(indexPath) && nodeFS3) {
|
|
7464
|
+
const fileContent = await nodeFS3.promises.readFile(indexPath, "utf8");
|
|
5421
7465
|
indexData = JSON.parse(fileContent);
|
|
5422
7466
|
} else {
|
|
5423
7467
|
const response = await fetch(indexPath);
|
|
@@ -5492,8 +7536,8 @@ var init_StaticDataUnpacker = __esm({
|
|
|
5492
7536
|
return null;
|
|
5493
7537
|
}
|
|
5494
7538
|
try {
|
|
5495
|
-
if (this.isLocalPath(attachmentPath) &&
|
|
5496
|
-
const buffer = await
|
|
7539
|
+
if (this.isLocalPath(attachmentPath) && nodeFS3) {
|
|
7540
|
+
const buffer = await nodeFS3.promises.readFile(attachmentPath);
|
|
5497
7541
|
return buffer;
|
|
5498
7542
|
} else {
|
|
5499
7543
|
const response = await fetch(attachmentPath);
|
|
@@ -5586,11 +7630,11 @@ var init_StaticDataUnpacker = __esm({
|
|
|
5586
7630
|
});
|
|
5587
7631
|
|
|
5588
7632
|
// src/impl/static/courseDB.ts
|
|
5589
|
-
var
|
|
7633
|
+
var import_common21, StaticCourseDB;
|
|
5590
7634
|
var init_courseDB3 = __esm({
|
|
5591
7635
|
"src/impl/static/courseDB.ts"() {
|
|
5592
7636
|
"use strict";
|
|
5593
|
-
|
|
7637
|
+
import_common21 = require("@vue-skuilder/common");
|
|
5594
7638
|
init_types_legacy();
|
|
5595
7639
|
init_logger();
|
|
5596
7640
|
init_defaults();
|
|
@@ -5851,7 +7895,7 @@ var init_courseDB3 = __esm({
|
|
|
5851
7895
|
}
|
|
5852
7896
|
async addNote(_codeCourse, _shape, _data, _author, _tags, _uploads, _elo) {
|
|
5853
7897
|
return {
|
|
5854
|
-
status:
|
|
7898
|
+
status: import_common21.Status.error,
|
|
5855
7899
|
message: "Cannot add notes in static mode"
|
|
5856
7900
|
};
|
|
5857
7901
|
}
|
|
@@ -5924,9 +7968,17 @@ var init_courseDB3 = __esm({
|
|
|
5924
7968
|
}
|
|
5925
7969
|
}
|
|
5926
7970
|
// Study Content Source implementation
|
|
7971
|
+
_pendingHints = null;
|
|
7972
|
+
setEphemeralHints(hints) {
|
|
7973
|
+
this._pendingHints = hints;
|
|
7974
|
+
}
|
|
5927
7975
|
async getWeightedCards(limit) {
|
|
5928
7976
|
try {
|
|
5929
7977
|
const navigator = await this.createNavigator(this.userDB);
|
|
7978
|
+
if (this._pendingHints) {
|
|
7979
|
+
navigator.setEphemeralHints(this._pendingHints);
|
|
7980
|
+
this._pendingHints = null;
|
|
7981
|
+
}
|
|
5930
7982
|
return navigator.getWeightedCards(limit);
|
|
5931
7983
|
} catch (e) {
|
|
5932
7984
|
logger.error(`[static/courseDB] Error getting weighted cards: ${e}`);
|