@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
package/dist/impl/couch/index.js
CHANGED
|
@@ -674,13 +674,20 @@ function captureRun(report) {
|
|
|
674
674
|
runHistory.pop();
|
|
675
675
|
}
|
|
676
676
|
}
|
|
677
|
-
function
|
|
677
|
+
function parseCardElo(provenance) {
|
|
678
|
+
const eloEntry = provenance.find((p) => p.strategy === "elo");
|
|
679
|
+
if (!eloEntry?.reason) return void 0;
|
|
680
|
+
const match = eloEntry.reason.match(/card:\s*(\d+)/);
|
|
681
|
+
return match ? parseInt(match[1], 10) : void 0;
|
|
682
|
+
}
|
|
683
|
+
function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
|
|
678
684
|
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
679
685
|
const cards = allCards.map((card) => ({
|
|
680
686
|
cardId: card.cardId,
|
|
681
687
|
courseId: card.courseId,
|
|
682
688
|
origin: getOrigin(card),
|
|
683
689
|
finalScore: card.score,
|
|
690
|
+
cardElo: parseCardElo(card.provenance),
|
|
684
691
|
provenance: card.provenance,
|
|
685
692
|
tags: card.tags,
|
|
686
693
|
selected: selectedIds.has(card.cardId)
|
|
@@ -690,6 +697,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
690
697
|
return {
|
|
691
698
|
courseId,
|
|
692
699
|
courseName,
|
|
700
|
+
userElo,
|
|
693
701
|
generatorName,
|
|
694
702
|
generators,
|
|
695
703
|
generatedCount,
|
|
@@ -710,6 +718,7 @@ function printRunSummary(run) {
|
|
|
710
718
|
console.group(`\u{1F50D} Pipeline Run: ${run.courseId} (${run.courseName || "unnamed"})`);
|
|
711
719
|
logger.info(`Run ID: ${run.runId}`);
|
|
712
720
|
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
721
|
+
logger.info(`User ELO: ${run.userElo ?? "unknown"}`);
|
|
713
722
|
logger.info(`Generator: ${run.generatorName} \u2192 ${run.generatedCount} candidates`);
|
|
714
723
|
if (run.generators && run.generators.length > 0) {
|
|
715
724
|
console.group("Generator breakdown:");
|
|
@@ -796,8 +805,12 @@ var init_PipelineDebugger = __esm({
|
|
|
796
805
|
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
797
806
|
logger.info(`Course: ${card.courseId}`);
|
|
798
807
|
logger.info(`Origin: ${card.origin}`);
|
|
808
|
+
logger.info(`Card ELO: ${card.cardElo ?? "unknown"} | User ELO: ${run.userElo ?? "unknown"}`);
|
|
799
809
|
logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
|
|
800
810
|
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
811
|
+
if (card.tags && card.tags.length > 0) {
|
|
812
|
+
logger.info(`Tags (${card.tags.length}): ${card.tags.join(", ")}`);
|
|
813
|
+
}
|
|
801
814
|
logger.info("Provenance:");
|
|
802
815
|
logger.info(formatProvenance(card.provenance));
|
|
803
816
|
console.groupEnd();
|
|
@@ -961,6 +974,27 @@ var init_PipelineDebugger = __esm({
|
|
|
961
974
|
}
|
|
962
975
|
return _activePipeline.diagnoseCardSpace({ threshold });
|
|
963
976
|
},
|
|
977
|
+
/**
|
|
978
|
+
* Show user's per-tag ELO data. Useful for diagnosing hierarchy gate status.
|
|
979
|
+
*
|
|
980
|
+
* @param tagFilter - Optional glob pattern(s) to filter tags.
|
|
981
|
+
* Examples: 'gpc:expose:*', 'gpc:intro:t-T', ['gpc:expose:t-*', 'gpc:intro:t-*']
|
|
982
|
+
*/
|
|
983
|
+
async showTagElo(tagFilter) {
|
|
984
|
+
if (!_activePipeline) {
|
|
985
|
+
logger.info("[Pipeline Debug] No active pipeline. Run a session first.");
|
|
986
|
+
return;
|
|
987
|
+
}
|
|
988
|
+
const status = await _activePipeline.getTagEloStatus(tagFilter);
|
|
989
|
+
const entries = Object.entries(status).sort(([a], [b]) => a.localeCompare(b));
|
|
990
|
+
if (entries.length === 0) {
|
|
991
|
+
logger.info(`[Pipeline Debug] No tag ELO data found${tagFilter ? ` for pattern: ${tagFilter}` : ""}.`);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
console.table(
|
|
995
|
+
Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
|
|
996
|
+
);
|
|
997
|
+
},
|
|
964
998
|
/**
|
|
965
999
|
* Show help.
|
|
966
1000
|
*/
|
|
@@ -972,6 +1006,7 @@ Commands:
|
|
|
972
1006
|
.showLastRun() Show summary of most recent pipeline run
|
|
973
1007
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
974
1008
|
.showCard(cardId) Show provenance trail for a specific card
|
|
1009
|
+
.showTagElo(pattern) Show user's tag ELO data (async). E.g. 'gpc:expose:*'
|
|
975
1010
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
976
1011
|
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
977
1012
|
.showRegistry() Show navigator registry (classes + roles)
|
|
@@ -1285,60 +1320,423 @@ var prescribed_exports = {};
|
|
|
1285
1320
|
__export(prescribed_exports, {
|
|
1286
1321
|
default: () => PrescribedCardsGenerator
|
|
1287
1322
|
});
|
|
1288
|
-
|
|
1323
|
+
function dedupe(arr) {
|
|
1324
|
+
return [...new Set(arr)];
|
|
1325
|
+
}
|
|
1326
|
+
function isoNow() {
|
|
1327
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1328
|
+
}
|
|
1329
|
+
function clamp(value, min, max) {
|
|
1330
|
+
return Math.max(min, Math.min(max, value));
|
|
1331
|
+
}
|
|
1332
|
+
function matchesTagPattern(tag, pattern) {
|
|
1333
|
+
if (pattern === "*") return true;
|
|
1334
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1335
|
+
const re = new RegExp(`^${escaped}$`);
|
|
1336
|
+
return re.test(tag);
|
|
1337
|
+
}
|
|
1338
|
+
function pickTopByScore(cards, limit) {
|
|
1339
|
+
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1340
|
+
}
|
|
1341
|
+
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;
|
|
1289
1342
|
var init_prescribed = __esm({
|
|
1290
1343
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1291
1344
|
"use strict";
|
|
1292
1345
|
init_navigators();
|
|
1293
1346
|
init_logger();
|
|
1347
|
+
DEFAULT_FRESHNESS_WINDOW = 3;
|
|
1348
|
+
DEFAULT_MAX_DIRECT_PER_RUN = 3;
|
|
1349
|
+
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1350
|
+
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1351
|
+
DEFAULT_MIN_COUNT = 3;
|
|
1352
|
+
BASE_TARGET_SCORE = 1;
|
|
1353
|
+
BASE_SUPPORT_SCORE = 0.8;
|
|
1354
|
+
MAX_TARGET_MULTIPLIER = 8;
|
|
1355
|
+
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1356
|
+
LOCKED_TAG_PREFIXES = ["concept:"];
|
|
1357
|
+
LESSON_GATE_PENALTY_TAG_HINT = "concept:";
|
|
1294
1358
|
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1295
1359
|
name;
|
|
1296
1360
|
config;
|
|
1297
1361
|
constructor(user, course, strategyData) {
|
|
1298
1362
|
super(user, course, strategyData);
|
|
1299
1363
|
this.name = strategyData.name || "Prescribed Cards";
|
|
1300
|
-
|
|
1301
|
-
const parsed = JSON.parse(strategyData.serializedData);
|
|
1302
|
-
this.config = { cardIds: parsed.cardIds || [] };
|
|
1303
|
-
} catch {
|
|
1304
|
-
this.config = { cardIds: [] };
|
|
1305
|
-
}
|
|
1364
|
+
this.config = this.parseConfig(strategyData.serializedData);
|
|
1306
1365
|
logger.debug(
|
|
1307
|
-
`[Prescribed] Initialized with ${this.config.
|
|
1366
|
+
`[Prescribed] Initialized with ${this.config.groups.length} groups and ${this.config.groups.reduce((n, g) => n + g.targetCardIds.length, 0)} targets`
|
|
1308
1367
|
);
|
|
1309
1368
|
}
|
|
1310
|
-
|
|
1311
|
-
|
|
1369
|
+
get strategyKey() {
|
|
1370
|
+
return "PrescribedProgress";
|
|
1371
|
+
}
|
|
1372
|
+
async getWeightedCards(limit, context) {
|
|
1373
|
+
if (this.config.groups.length === 0 || limit <= 0) {
|
|
1312
1374
|
return [];
|
|
1313
1375
|
}
|
|
1314
1376
|
const courseId = this.course.getCourseID();
|
|
1315
1377
|
const activeCards = await this.user.getActiveCards();
|
|
1316
1378
|
const activeIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
1317
|
-
const
|
|
1318
|
-
|
|
1319
|
-
|
|
1379
|
+
const seenCards = await this.user.getSeenCards(courseId).catch(() => []);
|
|
1380
|
+
const seenIds = new Set(seenCards);
|
|
1381
|
+
const progress = await this.getStrategyState() ?? {
|
|
1382
|
+
updatedAt: isoNow(),
|
|
1383
|
+
groups: {}
|
|
1384
|
+
};
|
|
1385
|
+
const hierarchyConfigs = await this.loadHierarchyConfigs();
|
|
1386
|
+
const courseReg = await this.user.getCourseRegDoc(courseId).catch(() => null);
|
|
1387
|
+
const userGlobalElo = typeof courseReg?.elo === "number" ? courseReg.elo : courseReg?.elo?.global?.score ?? context?.userElo ?? 1e3;
|
|
1388
|
+
const userTagElo = typeof courseReg?.elo === "number" ? {} : courseReg?.elo?.tags ?? {};
|
|
1389
|
+
const allTargetIds = dedupe(this.config.groups.flatMap((g) => g.targetCardIds));
|
|
1390
|
+
const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
|
|
1391
|
+
const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
|
|
1392
|
+
const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
|
|
1393
|
+
const nextState = {
|
|
1394
|
+
updatedAt: isoNow(),
|
|
1395
|
+
groups: {}
|
|
1396
|
+
};
|
|
1397
|
+
const emitted = [];
|
|
1398
|
+
const emittedIds = /* @__PURE__ */ new Set();
|
|
1399
|
+
for (const group of this.config.groups) {
|
|
1400
|
+
const runtime = this.buildGroupRuntimeState({
|
|
1401
|
+
group,
|
|
1402
|
+
priorState: progress.groups[group.id],
|
|
1403
|
+
activeIds,
|
|
1404
|
+
seenIds,
|
|
1405
|
+
tagsByCard,
|
|
1406
|
+
hierarchyConfigs,
|
|
1407
|
+
userTagElo,
|
|
1408
|
+
userGlobalElo
|
|
1409
|
+
});
|
|
1410
|
+
nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
|
|
1411
|
+
const directCards = this.buildDirectTargetCards(
|
|
1412
|
+
runtime,
|
|
1413
|
+
courseId,
|
|
1414
|
+
emittedIds
|
|
1415
|
+
);
|
|
1416
|
+
const supportCards = this.buildSupportCards(
|
|
1417
|
+
runtime,
|
|
1418
|
+
courseId,
|
|
1419
|
+
emittedIds
|
|
1420
|
+
);
|
|
1421
|
+
emitted.push(...directCards, ...supportCards);
|
|
1422
|
+
}
|
|
1423
|
+
if (emitted.length === 0) {
|
|
1424
|
+
logger.debug("[Prescribed] No prescribed targets/support emitted this run");
|
|
1425
|
+
await this.putStrategyState(nextState).catch((e) => {
|
|
1426
|
+
logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
|
|
1427
|
+
});
|
|
1320
1428
|
return [];
|
|
1321
1429
|
}
|
|
1322
|
-
const
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1430
|
+
const finalCards = pickTopByScore(emitted, limit);
|
|
1431
|
+
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
1432
|
+
for (const card of finalCards) {
|
|
1433
|
+
const prov = card.provenance[0];
|
|
1434
|
+
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
1435
|
+
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
1436
|
+
if (!groupId) continue;
|
|
1437
|
+
if (!surfacedByGroup.has(groupId)) {
|
|
1438
|
+
surfacedByGroup.set(groupId, { targetIds: [], supportIds: [] });
|
|
1439
|
+
}
|
|
1440
|
+
surfacedByGroup.get(groupId)[mode].push(card.cardId);
|
|
1441
|
+
}
|
|
1442
|
+
for (const group of this.config.groups) {
|
|
1443
|
+
const groupState = nextState.groups[group.id];
|
|
1444
|
+
const surfaced = surfacedByGroup.get(group.id);
|
|
1445
|
+
if (surfaced && (surfaced.targetIds.length > 0 || surfaced.supportIds.length > 0)) {
|
|
1446
|
+
groupState.lastSurfacedAt = isoNow();
|
|
1447
|
+
groupState.sessionsSinceSurfaced = 0;
|
|
1448
|
+
if (surfaced.supportIds.length > 0) {
|
|
1449
|
+
groupState.lastSupportAt = isoNow();
|
|
1334
1450
|
}
|
|
1335
|
-
|
|
1336
|
-
}
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
await this.putStrategyState(nextState).catch((e) => {
|
|
1454
|
+
logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
|
|
1455
|
+
});
|
|
1337
1456
|
logger.info(
|
|
1338
|
-
`[Prescribed] Emitting ${
|
|
1457
|
+
`[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)`
|
|
1339
1458
|
);
|
|
1459
|
+
return finalCards;
|
|
1460
|
+
}
|
|
1461
|
+
parseConfig(serializedData) {
|
|
1462
|
+
try {
|
|
1463
|
+
const parsed = JSON.parse(serializedData);
|
|
1464
|
+
const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
|
|
1465
|
+
const groups = groupsRaw.map((raw, i) => ({
|
|
1466
|
+
id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
|
|
1467
|
+
targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
|
|
1468
|
+
supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
|
|
1469
|
+
supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
|
|
1470
|
+
freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
|
|
1471
|
+
maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
|
|
1472
|
+
maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
|
|
1473
|
+
hierarchyWalk: {
|
|
1474
|
+
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
1475
|
+
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
1476
|
+
},
|
|
1477
|
+
retireOnEncounter: raw.retireOnEncounter !== false
|
|
1478
|
+
})).filter((g) => g.targetCardIds.length > 0);
|
|
1479
|
+
return { groups };
|
|
1480
|
+
} catch {
|
|
1481
|
+
return { groups: [] };
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
async loadHierarchyConfigs() {
|
|
1485
|
+
try {
|
|
1486
|
+
const strategies = await this.course.getNavigationStrategies();
|
|
1487
|
+
return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
|
|
1488
|
+
try {
|
|
1489
|
+
const parsed = JSON.parse(s.serializedData);
|
|
1490
|
+
return {
|
|
1491
|
+
prerequisites: parsed.prerequisites || {}
|
|
1492
|
+
};
|
|
1493
|
+
} catch {
|
|
1494
|
+
return { prerequisites: {} };
|
|
1495
|
+
}
|
|
1496
|
+
});
|
|
1497
|
+
} catch (e) {
|
|
1498
|
+
logger.debug(`[Prescribed] Failed to load hierarchy configs: ${e}`);
|
|
1499
|
+
return [];
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
buildGroupRuntimeState(args) {
|
|
1503
|
+
const {
|
|
1504
|
+
group,
|
|
1505
|
+
priorState,
|
|
1506
|
+
activeIds,
|
|
1507
|
+
seenIds,
|
|
1508
|
+
tagsByCard,
|
|
1509
|
+
hierarchyConfigs,
|
|
1510
|
+
userTagElo,
|
|
1511
|
+
userGlobalElo
|
|
1512
|
+
} = args;
|
|
1513
|
+
const encounteredTargets = /* @__PURE__ */ new Set();
|
|
1514
|
+
for (const cardId of group.targetCardIds) {
|
|
1515
|
+
if (activeIds.has(cardId) || seenIds.has(cardId)) {
|
|
1516
|
+
encounteredTargets.add(cardId);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
if (priorState?.encounteredCardIds?.length) {
|
|
1520
|
+
for (const cardId of priorState.encounteredCardIds) {
|
|
1521
|
+
encounteredTargets.add(cardId);
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
const pendingTargets = group.targetCardIds.filter((id) => !encounteredTargets.has(id));
|
|
1525
|
+
const targetTags = /* @__PURE__ */ new Map();
|
|
1526
|
+
for (const cardId of pendingTargets) {
|
|
1527
|
+
targetTags.set(cardId, tagsByCard.get(cardId) ?? []);
|
|
1528
|
+
}
|
|
1529
|
+
const blockedTargets = [];
|
|
1530
|
+
const surfaceableTargets = [];
|
|
1531
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1532
|
+
for (const cardId of pendingTargets) {
|
|
1533
|
+
const tags = targetTags.get(cardId) ?? [];
|
|
1534
|
+
const resolution = this.resolveBlockedSupportTags(
|
|
1535
|
+
tags,
|
|
1536
|
+
hierarchyConfigs,
|
|
1537
|
+
userTagElo,
|
|
1538
|
+
userGlobalElo,
|
|
1539
|
+
group.hierarchyWalk?.enabled !== false,
|
|
1540
|
+
group.hierarchyWalk?.maxDepth ?? DEFAULT_HIERARCHY_DEPTH
|
|
1541
|
+
);
|
|
1542
|
+
if (resolution.blocked) {
|
|
1543
|
+
blockedTargets.push(cardId);
|
|
1544
|
+
resolution.supportTags.forEach((t) => supportTags.add(t));
|
|
1545
|
+
} else {
|
|
1546
|
+
surfaceableTargets.push(cardId);
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
const supportCandidates = dedupe([
|
|
1550
|
+
...group.supportCardIds ?? [],
|
|
1551
|
+
...this.findSupportCardsByTags(
|
|
1552
|
+
group,
|
|
1553
|
+
tagsByCard,
|
|
1554
|
+
[...supportTags]
|
|
1555
|
+
)
|
|
1556
|
+
]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
|
|
1557
|
+
const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
|
|
1558
|
+
const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
1559
|
+
const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
|
|
1560
|
+
const pressureMultiplier = pendingTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.75 + Math.min(2, pendingTargets.length * 0.1), 1, MAX_TARGET_MULTIPLIER);
|
|
1561
|
+
const supportMultiplier = blockedTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.5 + Math.min(1.5, blockedTargets.length * 0.15), 1, MAX_SUPPORT_MULTIPLIER);
|
|
1562
|
+
return {
|
|
1563
|
+
group,
|
|
1564
|
+
encounteredTargets,
|
|
1565
|
+
pendingTargets,
|
|
1566
|
+
blockedTargets,
|
|
1567
|
+
surfaceableTargets,
|
|
1568
|
+
targetTags,
|
|
1569
|
+
supportCandidates,
|
|
1570
|
+
supportTags: [...supportTags],
|
|
1571
|
+
pressureMultiplier,
|
|
1572
|
+
supportMultiplier
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
buildNextGroupState(runtime, prior) {
|
|
1576
|
+
const carriedSessions = prior?.sessionsSinceSurfaced ?? 0;
|
|
1577
|
+
const surfacedThisRun = false;
|
|
1578
|
+
return {
|
|
1579
|
+
encounteredCardIds: [...runtime.encounteredTargets].sort(),
|
|
1580
|
+
lastSurfacedAt: prior?.lastSurfacedAt ?? null,
|
|
1581
|
+
sessionsSinceSurfaced: surfacedThisRun ? 0 : carriedSessions + 1,
|
|
1582
|
+
lastSupportAt: prior?.lastSupportAt ?? null,
|
|
1583
|
+
blockedTargetIds: [...runtime.blockedTargets].sort(),
|
|
1584
|
+
lastResolvedSupportTags: [...runtime.supportTags].sort()
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
buildDirectTargetCards(runtime, courseId, emittedIds) {
|
|
1588
|
+
const maxDirect = runtime.group.maxDirectTargetsPerRun ?? DEFAULT_MAX_DIRECT_PER_RUN;
|
|
1589
|
+
const directIds = runtime.surfaceableTargets.filter((id) => !emittedIds.has(id)).slice(0, maxDirect);
|
|
1590
|
+
const cards = [];
|
|
1591
|
+
for (const cardId of directIds) {
|
|
1592
|
+
emittedIds.add(cardId);
|
|
1593
|
+
cards.push({
|
|
1594
|
+
cardId,
|
|
1595
|
+
courseId,
|
|
1596
|
+
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1597
|
+
provenance: [
|
|
1598
|
+
{
|
|
1599
|
+
strategy: "prescribed",
|
|
1600
|
+
strategyName: this.strategyName || this.name,
|
|
1601
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1602
|
+
action: "generated",
|
|
1603
|
+
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1604
|
+
reason: `mode=target;group=${runtime.group.id};pending=${runtime.pendingTargets.length};surfaceable=${runtime.surfaceableTargets.length};blocked=${runtime.blockedTargets.length};multiplier=${runtime.pressureMultiplier.toFixed(2)}`
|
|
1605
|
+
}
|
|
1606
|
+
]
|
|
1607
|
+
});
|
|
1608
|
+
}
|
|
1609
|
+
return cards;
|
|
1610
|
+
}
|
|
1611
|
+
buildSupportCards(runtime, courseId, emittedIds) {
|
|
1612
|
+
if (runtime.blockedTargets.length === 0 || runtime.supportCandidates.length === 0) {
|
|
1613
|
+
return [];
|
|
1614
|
+
}
|
|
1615
|
+
const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
|
|
1616
|
+
const supportIds = runtime.supportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
|
|
1617
|
+
const cards = [];
|
|
1618
|
+
for (const cardId of supportIds) {
|
|
1619
|
+
emittedIds.add(cardId);
|
|
1620
|
+
cards.push({
|
|
1621
|
+
cardId,
|
|
1622
|
+
courseId,
|
|
1623
|
+
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1624
|
+
provenance: [
|
|
1625
|
+
{
|
|
1626
|
+
strategy: "prescribed",
|
|
1627
|
+
strategyName: this.strategyName || this.name,
|
|
1628
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1629
|
+
action: "generated",
|
|
1630
|
+
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1631
|
+
reason: `mode=support;group=${runtime.group.id};blocked=${runtime.blockedTargets.length};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.supportMultiplier.toFixed(2)}`
|
|
1632
|
+
}
|
|
1633
|
+
]
|
|
1634
|
+
});
|
|
1635
|
+
}
|
|
1340
1636
|
return cards;
|
|
1341
1637
|
}
|
|
1638
|
+
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
1639
|
+
if (supportTags.length === 0) {
|
|
1640
|
+
return [];
|
|
1641
|
+
}
|
|
1642
|
+
const explicitSupportIds = group.supportCardIds ?? [];
|
|
1643
|
+
const explicitPatterns = group.supportTagPatterns ?? [];
|
|
1644
|
+
if (explicitSupportIds.length === 0 && explicitPatterns.length === 0) {
|
|
1645
|
+
return [];
|
|
1646
|
+
}
|
|
1647
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1648
|
+
for (const cardId of explicitSupportIds) {
|
|
1649
|
+
const cardTags = tagsByCard.get(cardId) ?? [];
|
|
1650
|
+
const matchesResolved = supportTags.some((supportTag) => cardTags.includes(supportTag));
|
|
1651
|
+
const matchesPattern = explicitPatterns.some(
|
|
1652
|
+
(pattern) => cardTags.some((tag) => matchesTagPattern(tag, pattern))
|
|
1653
|
+
);
|
|
1654
|
+
if (matchesResolved || matchesPattern) {
|
|
1655
|
+
candidates.add(cardId);
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return [...candidates];
|
|
1659
|
+
}
|
|
1660
|
+
resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
|
|
1661
|
+
if (!hierarchyWalkEnabled || targetTags.length === 0 || hierarchyConfigs.length === 0) {
|
|
1662
|
+
return {
|
|
1663
|
+
blocked: false,
|
|
1664
|
+
supportTags: []
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1668
|
+
let blocked = false;
|
|
1669
|
+
for (const targetTag of targetTags) {
|
|
1670
|
+
for (const hierarchy of hierarchyConfigs) {
|
|
1671
|
+
const prereqs = hierarchy.prerequisites[targetTag];
|
|
1672
|
+
if (!prereqs || prereqs.length === 0) continue;
|
|
1673
|
+
const unmet = prereqs.filter(
|
|
1674
|
+
(pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo)
|
|
1675
|
+
);
|
|
1676
|
+
if (unmet.length === 0) {
|
|
1677
|
+
continue;
|
|
1678
|
+
}
|
|
1679
|
+
blocked = true;
|
|
1680
|
+
for (const prereq of unmet) {
|
|
1681
|
+
this.collectSupportTagsRecursive(
|
|
1682
|
+
prereq.tag,
|
|
1683
|
+
hierarchyConfigs,
|
|
1684
|
+
userTagElo,
|
|
1685
|
+
userGlobalElo,
|
|
1686
|
+
maxDepth,
|
|
1687
|
+
/* @__PURE__ */ new Set(),
|
|
1688
|
+
supportTags
|
|
1689
|
+
);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
return { blocked, supportTags: [...supportTags] };
|
|
1694
|
+
}
|
|
1695
|
+
collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
|
|
1696
|
+
if (depth < 0 || visited.has(tag)) return;
|
|
1697
|
+
if (this.isHardGatedTag(tag)) return;
|
|
1698
|
+
visited.add(tag);
|
|
1699
|
+
let walkedFurther = false;
|
|
1700
|
+
for (const hierarchy of hierarchyConfigs) {
|
|
1701
|
+
const prereqs = hierarchy.prerequisites[tag];
|
|
1702
|
+
if (!prereqs || prereqs.length === 0) continue;
|
|
1703
|
+
const unmet = prereqs.filter(
|
|
1704
|
+
(pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo)
|
|
1705
|
+
);
|
|
1706
|
+
if (unmet.length > 0 && depth > 0) {
|
|
1707
|
+
walkedFurther = true;
|
|
1708
|
+
for (const prereq of unmet) {
|
|
1709
|
+
this.collectSupportTagsRecursive(
|
|
1710
|
+
prereq.tag,
|
|
1711
|
+
hierarchyConfigs,
|
|
1712
|
+
userTagElo,
|
|
1713
|
+
userGlobalElo,
|
|
1714
|
+
depth - 1,
|
|
1715
|
+
visited,
|
|
1716
|
+
out
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (!walkedFurther) {
|
|
1722
|
+
out.add(tag);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
isHardGatedTag(tag) {
|
|
1726
|
+
return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
|
|
1727
|
+
}
|
|
1728
|
+
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
1729
|
+
if (!userTagElo) return false;
|
|
1730
|
+
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
|
|
1731
|
+
if (userTagElo.count < minCount) return false;
|
|
1732
|
+
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1733
|
+
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
1734
|
+
}
|
|
1735
|
+
if (prereq.masteryThreshold?.minCount !== void 0) {
|
|
1736
|
+
return true;
|
|
1737
|
+
}
|
|
1738
|
+
return userTagElo.score >= userGlobalElo;
|
|
1739
|
+
}
|
|
1342
1740
|
};
|
|
1343
1741
|
}
|
|
1344
1742
|
});
|
|
@@ -1701,13 +2099,14 @@ var hierarchyDefinition_exports = {};
|
|
|
1701
2099
|
__export(hierarchyDefinition_exports, {
|
|
1702
2100
|
default: () => HierarchyDefinitionNavigator
|
|
1703
2101
|
});
|
|
1704
|
-
var import_common6,
|
|
2102
|
+
var import_common6, DEFAULT_MIN_COUNT2, HierarchyDefinitionNavigator;
|
|
1705
2103
|
var init_hierarchyDefinition = __esm({
|
|
1706
2104
|
"src/core/navigators/filters/hierarchyDefinition.ts"() {
|
|
1707
2105
|
"use strict";
|
|
1708
2106
|
init_navigators();
|
|
1709
2107
|
import_common6 = require("@vue-skuilder/common");
|
|
1710
|
-
|
|
2108
|
+
init_logger();
|
|
2109
|
+
DEFAULT_MIN_COUNT2 = 3;
|
|
1711
2110
|
HierarchyDefinitionNavigator = class extends ContentNavigator {
|
|
1712
2111
|
config;
|
|
1713
2112
|
/** Human-readable name for CardFilter interface */
|
|
@@ -1734,7 +2133,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1734
2133
|
*/
|
|
1735
2134
|
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
1736
2135
|
if (!userTagElo) return false;
|
|
1737
|
-
const minCount = prereq.masteryThreshold?.minCount ??
|
|
2136
|
+
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT2;
|
|
1738
2137
|
if (userTagElo.count < minCount) return false;
|
|
1739
2138
|
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1740
2139
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
@@ -1835,18 +2234,55 @@ var init_hierarchyDefinition = __esm({
|
|
|
1835
2234
|
}
|
|
1836
2235
|
return boosts;
|
|
1837
2236
|
}
|
|
2237
|
+
/**
|
|
2238
|
+
* Build a map of gated tag → max configured targetBoost for all *open* gates.
|
|
2239
|
+
*
|
|
2240
|
+
* When a gate opens (prereqs met), cards carrying the gated tag get boosted —
|
|
2241
|
+
* ensuring newly-unlocked content surfaces promptly. The boost is a static
|
|
2242
|
+
* multiplier; natural ELO/SRS deprioritization after interaction handles decay.
|
|
2243
|
+
*/
|
|
2244
|
+
getTargetBoosts(unlockedTags) {
|
|
2245
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
2246
|
+
const configKeys = Object.keys(this.config.prerequisites);
|
|
2247
|
+
const unlockedArr = [...unlockedTags];
|
|
2248
|
+
logger.info(
|
|
2249
|
+
`[HierarchyDefinition:targetBoost:trace] ${this.name} | configKeys=${configKeys.length}, unlocked=${unlockedArr.length} (${unlockedArr.slice(0, 5).join(", ")}${unlockedArr.length > 5 ? "..." : ""})`
|
|
2250
|
+
);
|
|
2251
|
+
for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
|
|
2252
|
+
if (!unlockedTags.has(tagId)) continue;
|
|
2253
|
+
logger.info(
|
|
2254
|
+
`[HierarchyDefinition:targetBoost:trace] UNLOCKED ${tagId}: ${prereqs.length} prereqs, raw=${JSON.stringify(prereqs.map((p) => ({ tag: p.tag, tb: p.targetBoost })))}`
|
|
2255
|
+
);
|
|
2256
|
+
for (const prereq of prereqs) {
|
|
2257
|
+
if (!prereq.targetBoost || prereq.targetBoost <= 1) continue;
|
|
2258
|
+
const existing = boosts.get(tagId) ?? 1;
|
|
2259
|
+
boosts.set(tagId, Math.max(existing, prereq.targetBoost));
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
if (boosts.size > 0) {
|
|
2263
|
+
logger.info(
|
|
2264
|
+
`[HierarchyDefinition] targetBoosts active: ${[...boosts.entries()].map(([t, b]) => `${t}=\xD7${b}`).join(", ")}`
|
|
2265
|
+
);
|
|
2266
|
+
} else {
|
|
2267
|
+
logger.info(
|
|
2268
|
+
`[HierarchyDefinition:targetBoost:trace] no targetBoosts found despite ${unlockedArr.length} unlocked tags`
|
|
2269
|
+
);
|
|
2270
|
+
}
|
|
2271
|
+
return boosts;
|
|
2272
|
+
}
|
|
1838
2273
|
/**
|
|
1839
2274
|
* CardFilter.transform implementation.
|
|
1840
2275
|
*
|
|
1841
|
-
*
|
|
1842
|
-
* 1. Cards with locked tags receive score * 0.
|
|
1843
|
-
* 2. Cards carrying prereq tags of closed gates receive
|
|
1844
|
-
*
|
|
2276
|
+
* Three effects:
|
|
2277
|
+
* 1. Cards with locked tags receive score * 0.02 (gating penalty)
|
|
2278
|
+
* 2. Cards carrying prereq tags of closed gates receive preReqBoost
|
|
2279
|
+
* 3. Cards carrying gated tags of open gates receive targetBoost
|
|
1845
2280
|
*/
|
|
1846
2281
|
async transform(cards, context) {
|
|
1847
2282
|
const masteredTags = await this.getMasteredTags(context);
|
|
1848
2283
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
1849
2284
|
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
2285
|
+
const targetBoosts = this.getTargetBoosts(unlockedTags);
|
|
1850
2286
|
const gated = [];
|
|
1851
2287
|
for (const card of cards) {
|
|
1852
2288
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
@@ -1874,6 +2310,29 @@ var init_hierarchyDefinition = __esm({
|
|
|
1874
2310
|
finalScore *= maxBoost;
|
|
1875
2311
|
action = "boosted";
|
|
1876
2312
|
finalReason = `${reason} | preReqBoost \xD7${maxBoost.toFixed(2)} for ${boostedPrereqs.join(", ")}`;
|
|
2313
|
+
logger.info(
|
|
2314
|
+
`[HierarchyDefinition] preReqBoost \xD7${maxBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedPrereqs.join(", ")}] (score: ${card.score.toFixed(3)} \u2192 ${finalScore.toFixed(3)})`
|
|
2315
|
+
);
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
2318
|
+
if (isUnlocked && targetBoosts.size > 0) {
|
|
2319
|
+
const cardTags = card.tags ?? [];
|
|
2320
|
+
let maxTargetBoost = 1;
|
|
2321
|
+
const boostedTargets = [];
|
|
2322
|
+
for (const tag of cardTags) {
|
|
2323
|
+
const boost = targetBoosts.get(tag);
|
|
2324
|
+
if (boost && boost > maxTargetBoost) {
|
|
2325
|
+
maxTargetBoost = boost;
|
|
2326
|
+
boostedTargets.push(tag);
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
if (maxTargetBoost > 1) {
|
|
2330
|
+
finalScore *= maxTargetBoost;
|
|
2331
|
+
action = "boosted";
|
|
2332
|
+
finalReason = `${finalReason} | targetBoost \xD7${maxTargetBoost.toFixed(2)} for ${boostedTargets.join(", ")}`;
|
|
2333
|
+
logger.info(
|
|
2334
|
+
`[HierarchyDefinition] targetBoost \xD7${maxTargetBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedTargets.join(", ")}] (score: ${card.score.toFixed(3)} \u2192 ${finalScore.toFixed(3)})`
|
|
2335
|
+
);
|
|
1877
2336
|
}
|
|
1878
2337
|
}
|
|
1879
2338
|
gated.push({
|
|
@@ -2060,13 +2519,13 @@ var interferenceMitigator_exports = {};
|
|
|
2060
2519
|
__export(interferenceMitigator_exports, {
|
|
2061
2520
|
default: () => InterferenceMitigatorNavigator
|
|
2062
2521
|
});
|
|
2063
|
-
var import_common7,
|
|
2522
|
+
var import_common7, DEFAULT_MIN_COUNT3, DEFAULT_MIN_ELAPSED_DAYS, DEFAULT_INTERFERENCE_DECAY, InterferenceMitigatorNavigator;
|
|
2064
2523
|
var init_interferenceMitigator = __esm({
|
|
2065
2524
|
"src/core/navigators/filters/interferenceMitigator.ts"() {
|
|
2066
2525
|
"use strict";
|
|
2067
2526
|
init_navigators();
|
|
2068
2527
|
import_common7 = require("@vue-skuilder/common");
|
|
2069
|
-
|
|
2528
|
+
DEFAULT_MIN_COUNT3 = 10;
|
|
2070
2529
|
DEFAULT_MIN_ELAPSED_DAYS = 3;
|
|
2071
2530
|
DEFAULT_INTERFERENCE_DECAY = 0.8;
|
|
2072
2531
|
InterferenceMitigatorNavigator = class extends ContentNavigator {
|
|
@@ -2091,7 +2550,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2091
2550
|
return {
|
|
2092
2551
|
interferenceSets: sets,
|
|
2093
2552
|
maturityThreshold: {
|
|
2094
|
-
minCount: parsed.maturityThreshold?.minCount ??
|
|
2553
|
+
minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3,
|
|
2095
2554
|
minElo: parsed.maturityThreshold?.minElo,
|
|
2096
2555
|
minElapsedDays: parsed.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS
|
|
2097
2556
|
},
|
|
@@ -2101,7 +2560,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2101
2560
|
return {
|
|
2102
2561
|
interferenceSets: [],
|
|
2103
2562
|
maturityThreshold: {
|
|
2104
|
-
minCount:
|
|
2563
|
+
minCount: DEFAULT_MIN_COUNT3,
|
|
2105
2564
|
minElapsedDays: DEFAULT_MIN_ELAPSED_DAYS
|
|
2106
2565
|
},
|
|
2107
2566
|
defaultDecay: DEFAULT_INTERFERENCE_DECAY
|
|
@@ -2148,7 +2607,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2148
2607
|
try {
|
|
2149
2608
|
const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
|
|
2150
2609
|
const userElo = (0, import_common7.toCourseElo)(courseReg.elo);
|
|
2151
|
-
const minCount = this.config.maturityThreshold?.minCount ??
|
|
2610
|
+
const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3;
|
|
2152
2611
|
const minElo = this.config.maturityThreshold?.minElo;
|
|
2153
2612
|
const minElapsedDays = this.config.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS;
|
|
2154
2613
|
const minCountForElapsed = minElapsedDays * 2;
|
|
@@ -2383,7 +2842,7 @@ var init_relativePriority = __esm({
|
|
|
2383
2842
|
const cardTags = card.tags ?? [];
|
|
2384
2843
|
const priority = this.computeCardPriority(cardTags);
|
|
2385
2844
|
const boostFactor = this.computeBoostFactor(priority);
|
|
2386
|
-
const finalScore = Math.max(0,
|
|
2845
|
+
const finalScore = Math.max(0, card.score * boostFactor);
|
|
2387
2846
|
const action = boostFactor > 1 ? "boosted" : boostFactor < 1 ? "penalized" : "passed";
|
|
2388
2847
|
const reason = this.buildPriorityReason(cardTags, priority, boostFactor, finalScore);
|
|
2389
2848
|
return {
|
|
@@ -2477,96 +2936,1683 @@ var init_learning = __esm({
|
|
|
2477
2936
|
}
|
|
2478
2937
|
});
|
|
2479
2938
|
|
|
2480
|
-
// src/core/orchestration/signal.ts
|
|
2481
|
-
var init_signal = __esm({
|
|
2482
|
-
"src/core/orchestration/signal.ts"() {
|
|
2483
|
-
"use strict";
|
|
2484
|
-
}
|
|
2485
|
-
});
|
|
2939
|
+
// src/core/orchestration/signal.ts
|
|
2940
|
+
var init_signal = __esm({
|
|
2941
|
+
"src/core/orchestration/signal.ts"() {
|
|
2942
|
+
"use strict";
|
|
2943
|
+
}
|
|
2944
|
+
});
|
|
2945
|
+
|
|
2946
|
+
// src/core/orchestration/recording.ts
|
|
2947
|
+
var init_recording = __esm({
|
|
2948
|
+
"src/core/orchestration/recording.ts"() {
|
|
2949
|
+
"use strict";
|
|
2950
|
+
init_signal();
|
|
2951
|
+
init_types_legacy();
|
|
2952
|
+
init_logger();
|
|
2953
|
+
}
|
|
2954
|
+
});
|
|
2955
|
+
|
|
2956
|
+
// src/core/orchestration/index.ts
|
|
2957
|
+
function fnv1a(str) {
|
|
2958
|
+
let hash = 2166136261;
|
|
2959
|
+
for (let i = 0; i < str.length; i++) {
|
|
2960
|
+
hash ^= str.charCodeAt(i);
|
|
2961
|
+
hash = Math.imul(hash, 16777619);
|
|
2962
|
+
}
|
|
2963
|
+
return hash >>> 0;
|
|
2964
|
+
}
|
|
2965
|
+
function computeDeviation(userId, strategyId, salt) {
|
|
2966
|
+
const input = `${userId}:${strategyId}:${salt}`;
|
|
2967
|
+
const hash = fnv1a(input);
|
|
2968
|
+
const normalized = hash / 4294967296;
|
|
2969
|
+
return normalized * 2 - 1;
|
|
2970
|
+
}
|
|
2971
|
+
function computeSpread(confidence) {
|
|
2972
|
+
const clampedConfidence = Math.max(0, Math.min(1, confidence));
|
|
2973
|
+
return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
|
|
2974
|
+
}
|
|
2975
|
+
function computeEffectiveWeight(learnable, userId, strategyId, salt) {
|
|
2976
|
+
const deviation = computeDeviation(userId, strategyId, salt);
|
|
2977
|
+
const spread = computeSpread(learnable.confidence);
|
|
2978
|
+
const adjustment = deviation * spread * learnable.weight;
|
|
2979
|
+
const effective = learnable.weight + adjustment;
|
|
2980
|
+
return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
|
|
2981
|
+
}
|
|
2982
|
+
async function createOrchestrationContext(user, course) {
|
|
2983
|
+
let courseConfig;
|
|
2984
|
+
try {
|
|
2985
|
+
courseConfig = await course.getCourseConfig();
|
|
2986
|
+
} catch (e) {
|
|
2987
|
+
logger.error(`[Orchestration] Failed to load course config: ${e}`);
|
|
2988
|
+
courseConfig = {
|
|
2989
|
+
name: "Unknown",
|
|
2990
|
+
description: "",
|
|
2991
|
+
public: false,
|
|
2992
|
+
deleted: false,
|
|
2993
|
+
creator: "",
|
|
2994
|
+
admins: [],
|
|
2995
|
+
moderators: [],
|
|
2996
|
+
dataShapes: [],
|
|
2997
|
+
questionTypes: [],
|
|
2998
|
+
orchestration: { salt: "default" }
|
|
2999
|
+
};
|
|
3000
|
+
}
|
|
3001
|
+
const userId = user.getUsername();
|
|
3002
|
+
const salt = courseConfig.orchestration?.salt || "default_salt";
|
|
3003
|
+
return {
|
|
3004
|
+
user,
|
|
3005
|
+
course,
|
|
3006
|
+
userId,
|
|
3007
|
+
courseConfig,
|
|
3008
|
+
getEffectiveWeight(strategyId, learnable) {
|
|
3009
|
+
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
3010
|
+
},
|
|
3011
|
+
getDeviation(strategyId) {
|
|
3012
|
+
return computeDeviation(userId, strategyId, salt);
|
|
3013
|
+
}
|
|
3014
|
+
};
|
|
3015
|
+
}
|
|
3016
|
+
var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
|
|
3017
|
+
var init_orchestration = __esm({
|
|
3018
|
+
"src/core/orchestration/index.ts"() {
|
|
3019
|
+
"use strict";
|
|
3020
|
+
init_logger();
|
|
3021
|
+
init_gradient();
|
|
3022
|
+
init_learning();
|
|
3023
|
+
init_signal();
|
|
3024
|
+
init_recording();
|
|
3025
|
+
MIN_SPREAD = 0.1;
|
|
3026
|
+
MAX_SPREAD = 0.5;
|
|
3027
|
+
MIN_WEIGHT = 0.1;
|
|
3028
|
+
MAX_WEIGHT = 3;
|
|
3029
|
+
}
|
|
3030
|
+
});
|
|
3031
|
+
|
|
3032
|
+
// src/core/util/index.ts
|
|
3033
|
+
function getCardHistoryID(courseID, cardID) {
|
|
3034
|
+
return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
|
|
3035
|
+
}
|
|
3036
|
+
var init_util = __esm({
|
|
3037
|
+
"src/core/util/index.ts"() {
|
|
3038
|
+
"use strict";
|
|
3039
|
+
init_types_legacy();
|
|
3040
|
+
}
|
|
3041
|
+
});
|
|
3042
|
+
|
|
3043
|
+
// src/study/SpacedRepetition.ts
|
|
3044
|
+
var import_moment2, import_common8, duration;
|
|
3045
|
+
var init_SpacedRepetition = __esm({
|
|
3046
|
+
"src/study/SpacedRepetition.ts"() {
|
|
3047
|
+
"use strict";
|
|
3048
|
+
init_util();
|
|
3049
|
+
import_moment2 = __toESM(require("moment"), 1);
|
|
3050
|
+
import_common8 = require("@vue-skuilder/common");
|
|
3051
|
+
init_logger();
|
|
3052
|
+
duration = import_moment2.default.duration;
|
|
3053
|
+
}
|
|
3054
|
+
});
|
|
3055
|
+
|
|
3056
|
+
// src/study/services/SrsService.ts
|
|
3057
|
+
var import_moment3;
|
|
3058
|
+
var init_SrsService = __esm({
|
|
3059
|
+
"src/study/services/SrsService.ts"() {
|
|
3060
|
+
"use strict";
|
|
3061
|
+
import_moment3 = __toESM(require("moment"), 1);
|
|
3062
|
+
init_couch();
|
|
3063
|
+
init_SpacedRepetition();
|
|
3064
|
+
init_logger();
|
|
3065
|
+
}
|
|
3066
|
+
});
|
|
3067
|
+
|
|
3068
|
+
// src/study/services/EloService.ts
|
|
3069
|
+
var import_common9;
|
|
3070
|
+
var init_EloService = __esm({
|
|
3071
|
+
"src/study/services/EloService.ts"() {
|
|
3072
|
+
"use strict";
|
|
3073
|
+
import_common9 = require("@vue-skuilder/common");
|
|
3074
|
+
init_logger();
|
|
3075
|
+
}
|
|
3076
|
+
});
|
|
3077
|
+
|
|
3078
|
+
// src/study/services/ResponseProcessor.ts
|
|
3079
|
+
var import_common10;
|
|
3080
|
+
var init_ResponseProcessor = __esm({
|
|
3081
|
+
"src/study/services/ResponseProcessor.ts"() {
|
|
3082
|
+
"use strict";
|
|
3083
|
+
init_core();
|
|
3084
|
+
init_logger();
|
|
3085
|
+
import_common10 = require("@vue-skuilder/common");
|
|
3086
|
+
}
|
|
3087
|
+
});
|
|
3088
|
+
|
|
3089
|
+
// src/study/services/CardHydrationService.ts
|
|
3090
|
+
var import_common11;
|
|
3091
|
+
var init_CardHydrationService = __esm({
|
|
3092
|
+
"src/study/services/CardHydrationService.ts"() {
|
|
3093
|
+
"use strict";
|
|
3094
|
+
import_common11 = require("@vue-skuilder/common");
|
|
3095
|
+
init_logger();
|
|
3096
|
+
}
|
|
3097
|
+
});
|
|
3098
|
+
|
|
3099
|
+
// src/study/ItemQueue.ts
|
|
3100
|
+
var init_ItemQueue = __esm({
|
|
3101
|
+
"src/study/ItemQueue.ts"() {
|
|
3102
|
+
"use strict";
|
|
3103
|
+
}
|
|
3104
|
+
});
|
|
3105
|
+
|
|
3106
|
+
// src/util/packer/types.ts
|
|
3107
|
+
var init_types3 = __esm({
|
|
3108
|
+
"src/util/packer/types.ts"() {
|
|
3109
|
+
"use strict";
|
|
3110
|
+
}
|
|
3111
|
+
});
|
|
3112
|
+
|
|
3113
|
+
// src/util/packer/CouchDBToStaticPacker.ts
|
|
3114
|
+
var init_CouchDBToStaticPacker = __esm({
|
|
3115
|
+
"src/util/packer/CouchDBToStaticPacker.ts"() {
|
|
3116
|
+
"use strict";
|
|
3117
|
+
init_types_legacy();
|
|
3118
|
+
init_logger();
|
|
3119
|
+
}
|
|
3120
|
+
});
|
|
3121
|
+
|
|
3122
|
+
// src/util/packer/index.ts
|
|
3123
|
+
var init_packer = __esm({
|
|
3124
|
+
"src/util/packer/index.ts"() {
|
|
3125
|
+
"use strict";
|
|
3126
|
+
init_types3();
|
|
3127
|
+
init_CouchDBToStaticPacker();
|
|
3128
|
+
}
|
|
3129
|
+
});
|
|
3130
|
+
|
|
3131
|
+
// src/util/migrator/types.ts
|
|
3132
|
+
var DEFAULT_MIGRATION_OPTIONS;
|
|
3133
|
+
var init_types4 = __esm({
|
|
3134
|
+
"src/util/migrator/types.ts"() {
|
|
3135
|
+
"use strict";
|
|
3136
|
+
DEFAULT_MIGRATION_OPTIONS = {
|
|
3137
|
+
chunkBatchSize: 100,
|
|
3138
|
+
validateRoundTrip: false,
|
|
3139
|
+
cleanupOnFailure: true,
|
|
3140
|
+
timeout: 3e5
|
|
3141
|
+
// 5 minutes
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
});
|
|
3145
|
+
|
|
3146
|
+
// src/util/migrator/FileSystemAdapter.ts
|
|
3147
|
+
var FileSystemError;
|
|
3148
|
+
var init_FileSystemAdapter = __esm({
|
|
3149
|
+
"src/util/migrator/FileSystemAdapter.ts"() {
|
|
3150
|
+
"use strict";
|
|
3151
|
+
FileSystemError = class extends Error {
|
|
3152
|
+
constructor(message, operation, filePath, cause) {
|
|
3153
|
+
super(message);
|
|
3154
|
+
this.operation = operation;
|
|
3155
|
+
this.filePath = filePath;
|
|
3156
|
+
this.cause = cause;
|
|
3157
|
+
this.name = "FileSystemError";
|
|
3158
|
+
}
|
|
3159
|
+
};
|
|
3160
|
+
}
|
|
3161
|
+
});
|
|
3162
|
+
|
|
3163
|
+
// src/util/migrator/validation.ts
|
|
3164
|
+
async function validateStaticCourse(staticPath, fs) {
|
|
3165
|
+
const validation = {
|
|
3166
|
+
valid: true,
|
|
3167
|
+
manifestExists: false,
|
|
3168
|
+
chunksExist: false,
|
|
3169
|
+
attachmentsExist: false,
|
|
3170
|
+
errors: [],
|
|
3171
|
+
warnings: []
|
|
3172
|
+
};
|
|
3173
|
+
try {
|
|
3174
|
+
if (fs) {
|
|
3175
|
+
const stats = await fs.stat(staticPath);
|
|
3176
|
+
if (!stats.isDirectory()) {
|
|
3177
|
+
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3178
|
+
validation.valid = false;
|
|
3179
|
+
return validation;
|
|
3180
|
+
}
|
|
3181
|
+
} else if (!nodeFS) {
|
|
3182
|
+
validation.errors.push("File system access not available - validation skipped");
|
|
3183
|
+
validation.valid = false;
|
|
3184
|
+
return validation;
|
|
3185
|
+
} else {
|
|
3186
|
+
const stats = await nodeFS.promises.stat(staticPath);
|
|
3187
|
+
if (!stats.isDirectory()) {
|
|
3188
|
+
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3189
|
+
validation.valid = false;
|
|
3190
|
+
return validation;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
let manifestPath = `${staticPath}/manifest.json`;
|
|
3194
|
+
try {
|
|
3195
|
+
if (fs) {
|
|
3196
|
+
manifestPath = fs.joinPath(staticPath, "manifest.json");
|
|
3197
|
+
if (await fs.exists(manifestPath)) {
|
|
3198
|
+
validation.manifestExists = true;
|
|
3199
|
+
const manifestContent = await fs.readFile(manifestPath);
|
|
3200
|
+
const manifest = JSON.parse(manifestContent);
|
|
3201
|
+
validation.courseId = manifest.courseId;
|
|
3202
|
+
validation.courseName = manifest.courseName;
|
|
3203
|
+
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3204
|
+
validation.errors.push("Invalid manifest structure");
|
|
3205
|
+
validation.valid = false;
|
|
3206
|
+
}
|
|
3207
|
+
} else {
|
|
3208
|
+
validation.errors.push(`Manifest not found: ${manifestPath}`);
|
|
3209
|
+
validation.valid = false;
|
|
3210
|
+
}
|
|
3211
|
+
} else {
|
|
3212
|
+
manifestPath = `${staticPath}/manifest.json`;
|
|
3213
|
+
await nodeFS.promises.access(manifestPath);
|
|
3214
|
+
validation.manifestExists = true;
|
|
3215
|
+
const manifestContent = await nodeFS.promises.readFile(manifestPath, "utf8");
|
|
3216
|
+
const manifest = JSON.parse(manifestContent);
|
|
3217
|
+
validation.courseId = manifest.courseId;
|
|
3218
|
+
validation.courseName = manifest.courseName;
|
|
3219
|
+
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3220
|
+
validation.errors.push("Invalid manifest structure");
|
|
3221
|
+
validation.valid = false;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
} catch (error) {
|
|
3225
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Manifest not found or invalid: ${manifestPath}`;
|
|
3226
|
+
validation.errors.push(errorMessage);
|
|
3227
|
+
validation.valid = false;
|
|
3228
|
+
}
|
|
3229
|
+
let chunksPath = `${staticPath}/chunks`;
|
|
3230
|
+
try {
|
|
3231
|
+
if (fs) {
|
|
3232
|
+
chunksPath = fs.joinPath(staticPath, "chunks");
|
|
3233
|
+
if (await fs.exists(chunksPath)) {
|
|
3234
|
+
const chunksStats = await fs.stat(chunksPath);
|
|
3235
|
+
if (chunksStats.isDirectory()) {
|
|
3236
|
+
validation.chunksExist = true;
|
|
3237
|
+
} else {
|
|
3238
|
+
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3239
|
+
validation.valid = false;
|
|
3240
|
+
}
|
|
3241
|
+
} else {
|
|
3242
|
+
validation.errors.push(`Chunks directory not found: ${chunksPath}`);
|
|
3243
|
+
validation.valid = false;
|
|
3244
|
+
}
|
|
3245
|
+
} else {
|
|
3246
|
+
chunksPath = `${staticPath}/chunks`;
|
|
3247
|
+
const chunksStats = await nodeFS.promises.stat(chunksPath);
|
|
3248
|
+
if (chunksStats.isDirectory()) {
|
|
3249
|
+
validation.chunksExist = true;
|
|
3250
|
+
} else {
|
|
3251
|
+
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3252
|
+
validation.valid = false;
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
} catch (error) {
|
|
3256
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Chunks directory not found: ${chunksPath}`;
|
|
3257
|
+
validation.errors.push(errorMessage);
|
|
3258
|
+
validation.valid = false;
|
|
3259
|
+
}
|
|
3260
|
+
let attachmentsPath;
|
|
3261
|
+
try {
|
|
3262
|
+
if (fs) {
|
|
3263
|
+
attachmentsPath = fs.joinPath(staticPath, "attachments");
|
|
3264
|
+
if (await fs.exists(attachmentsPath)) {
|
|
3265
|
+
const attachmentsStats = await fs.stat(attachmentsPath);
|
|
3266
|
+
if (attachmentsStats.isDirectory()) {
|
|
3267
|
+
validation.attachmentsExist = true;
|
|
3268
|
+
}
|
|
3269
|
+
} else {
|
|
3270
|
+
validation.warnings.push(
|
|
3271
|
+
`Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`
|
|
3272
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
} else {
|
|
3275
|
+
attachmentsPath = `${staticPath}/attachments`;
|
|
3276
|
+
const attachmentsStats = await nodeFS.promises.stat(attachmentsPath);
|
|
3277
|
+
if (attachmentsStats.isDirectory()) {
|
|
3278
|
+
validation.attachmentsExist = true;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
} catch (error) {
|
|
3282
|
+
attachmentsPath = attachmentsPath || `${staticPath}/attachments`;
|
|
3283
|
+
const warningMessage = error instanceof FileSystemError ? error.message : `Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`;
|
|
3284
|
+
validation.warnings.push(warningMessage);
|
|
3285
|
+
}
|
|
3286
|
+
} catch (error) {
|
|
3287
|
+
validation.errors.push(
|
|
3288
|
+
`Failed to validate static course: ${error instanceof Error ? error.message : String(error)}`
|
|
3289
|
+
);
|
|
3290
|
+
validation.valid = false;
|
|
3291
|
+
}
|
|
3292
|
+
return validation;
|
|
3293
|
+
}
|
|
3294
|
+
async function validateMigration(targetDB, expectedCounts, manifest) {
|
|
3295
|
+
const validation = {
|
|
3296
|
+
valid: true,
|
|
3297
|
+
documentCountMatch: false,
|
|
3298
|
+
attachmentIntegrity: false,
|
|
3299
|
+
viewFunctionality: false,
|
|
3300
|
+
issues: []
|
|
3301
|
+
};
|
|
3302
|
+
try {
|
|
3303
|
+
logger.info("Starting migration validation...");
|
|
3304
|
+
const actualCounts = await getActualDocumentCounts(targetDB);
|
|
3305
|
+
validation.documentCountMatch = compareDocumentCounts(
|
|
3306
|
+
expectedCounts,
|
|
3307
|
+
actualCounts,
|
|
3308
|
+
validation.issues
|
|
3309
|
+
);
|
|
3310
|
+
await validateCourseConfig(targetDB, manifest, validation.issues);
|
|
3311
|
+
validation.viewFunctionality = await validateViews(targetDB, manifest, validation.issues);
|
|
3312
|
+
validation.attachmentIntegrity = await validateAttachmentIntegrity(targetDB, validation.issues);
|
|
3313
|
+
validation.valid = validation.documentCountMatch && validation.viewFunctionality && validation.attachmentIntegrity;
|
|
3314
|
+
logger.info(`Migration validation completed. Valid: ${validation.valid}`);
|
|
3315
|
+
if (validation.issues.length > 0) {
|
|
3316
|
+
logger.info(`Validation issues: ${validation.issues.length}`);
|
|
3317
|
+
validation.issues.forEach((issue) => {
|
|
3318
|
+
if (issue.type === "error") {
|
|
3319
|
+
logger.error(`${issue.category}: ${issue.message}`);
|
|
3320
|
+
} else {
|
|
3321
|
+
logger.warn(`${issue.category}: ${issue.message}`);
|
|
3322
|
+
}
|
|
3323
|
+
});
|
|
3324
|
+
}
|
|
3325
|
+
} catch (error) {
|
|
3326
|
+
validation.valid = false;
|
|
3327
|
+
validation.issues.push({
|
|
3328
|
+
type: "error",
|
|
3329
|
+
category: "metadata",
|
|
3330
|
+
message: `Validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3331
|
+
});
|
|
3332
|
+
}
|
|
3333
|
+
return validation;
|
|
3334
|
+
}
|
|
3335
|
+
async function getActualDocumentCounts(db) {
|
|
3336
|
+
const counts = {};
|
|
3337
|
+
try {
|
|
3338
|
+
const allDocs = await db.allDocs({ include_docs: true });
|
|
3339
|
+
for (const row of allDocs.rows) {
|
|
3340
|
+
if (row.id.startsWith("_design/")) {
|
|
3341
|
+
counts["_design"] = (counts["_design"] || 0) + 1;
|
|
3342
|
+
continue;
|
|
3343
|
+
}
|
|
3344
|
+
const doc = row.doc;
|
|
3345
|
+
if (doc && doc.docType) {
|
|
3346
|
+
counts[doc.docType] = (counts[doc.docType] || 0) + 1;
|
|
3347
|
+
} else {
|
|
3348
|
+
counts["unknown"] = (counts["unknown"] || 0) + 1;
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
} catch (error) {
|
|
3352
|
+
logger.error("Failed to get actual document counts:", error);
|
|
3353
|
+
}
|
|
3354
|
+
return counts;
|
|
3355
|
+
}
|
|
3356
|
+
function compareDocumentCounts(expected, actual, issues) {
|
|
3357
|
+
let countsMatch = true;
|
|
3358
|
+
for (const [docType, expectedCount] of Object.entries(expected)) {
|
|
3359
|
+
const actualCount = actual[docType] || 0;
|
|
3360
|
+
if (actualCount !== expectedCount) {
|
|
3361
|
+
countsMatch = false;
|
|
3362
|
+
issues.push({
|
|
3363
|
+
type: "error",
|
|
3364
|
+
category: "documents",
|
|
3365
|
+
message: `Document count mismatch for ${docType}: expected ${expectedCount}, got ${actualCount}`
|
|
3366
|
+
});
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
for (const [docType, actualCount] of Object.entries(actual)) {
|
|
3370
|
+
if (!expected[docType] && docType !== "_design") {
|
|
3371
|
+
issues.push({
|
|
3372
|
+
type: "warning",
|
|
3373
|
+
category: "documents",
|
|
3374
|
+
message: `Unexpected document type found: ${docType} (${actualCount} documents)`
|
|
3375
|
+
});
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
return countsMatch;
|
|
3379
|
+
}
|
|
3380
|
+
async function validateCourseConfig(db, manifest, issues) {
|
|
3381
|
+
try {
|
|
3382
|
+
const courseConfig = await db.get("CourseConfig");
|
|
3383
|
+
if (!courseConfig) {
|
|
3384
|
+
issues.push({
|
|
3385
|
+
type: "error",
|
|
3386
|
+
category: "course_config",
|
|
3387
|
+
message: "CourseConfig document not found after migration"
|
|
3388
|
+
});
|
|
3389
|
+
return;
|
|
3390
|
+
}
|
|
3391
|
+
if (!courseConfig.courseID) {
|
|
3392
|
+
issues.push({
|
|
3393
|
+
type: "warning",
|
|
3394
|
+
category: "course_config",
|
|
3395
|
+
message: "CourseConfig document missing courseID field"
|
|
3396
|
+
});
|
|
3397
|
+
}
|
|
3398
|
+
if (courseConfig.courseID !== manifest.courseId) {
|
|
3399
|
+
issues.push({
|
|
3400
|
+
type: "warning",
|
|
3401
|
+
category: "course_config",
|
|
3402
|
+
message: `CourseConfig courseID mismatch: expected ${manifest.courseId}, got ${courseConfig.courseID}`
|
|
3403
|
+
});
|
|
3404
|
+
}
|
|
3405
|
+
logger.debug("CourseConfig document validation passed");
|
|
3406
|
+
} catch (error) {
|
|
3407
|
+
if (error.status === 404) {
|
|
3408
|
+
issues.push({
|
|
3409
|
+
type: "error",
|
|
3410
|
+
category: "course_config",
|
|
3411
|
+
message: "CourseConfig document not found in database"
|
|
3412
|
+
});
|
|
3413
|
+
} else {
|
|
3414
|
+
issues.push({
|
|
3415
|
+
type: "error",
|
|
3416
|
+
category: "course_config",
|
|
3417
|
+
message: `Failed to validate CourseConfig document: ${error instanceof Error ? error.message : String(error)}`
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
async function validateViews(db, manifest, issues) {
|
|
3423
|
+
let viewsValid = true;
|
|
3424
|
+
try {
|
|
3425
|
+
for (const designDoc of manifest.designDocs) {
|
|
3426
|
+
try {
|
|
3427
|
+
const doc = await db.get(designDoc._id);
|
|
3428
|
+
if (!doc) {
|
|
3429
|
+
viewsValid = false;
|
|
3430
|
+
issues.push({
|
|
3431
|
+
type: "error",
|
|
3432
|
+
category: "views",
|
|
3433
|
+
message: `Design document not found: ${designDoc._id}`
|
|
3434
|
+
});
|
|
3435
|
+
continue;
|
|
3436
|
+
}
|
|
3437
|
+
for (const viewName of Object.keys(designDoc.views)) {
|
|
3438
|
+
try {
|
|
3439
|
+
const viewPath = `${designDoc._id}/${viewName}`;
|
|
3440
|
+
await db.query(viewPath, { limit: 1 });
|
|
3441
|
+
} catch (viewError) {
|
|
3442
|
+
viewsValid = false;
|
|
3443
|
+
issues.push({
|
|
3444
|
+
type: "error",
|
|
3445
|
+
category: "views",
|
|
3446
|
+
message: `View not accessible: ${designDoc._id}/${viewName} - ${viewError}`
|
|
3447
|
+
});
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
} catch (error) {
|
|
3451
|
+
viewsValid = false;
|
|
3452
|
+
issues.push({
|
|
3453
|
+
type: "error",
|
|
3454
|
+
category: "views",
|
|
3455
|
+
message: `Failed to validate design document ${designDoc._id}: ${error}`
|
|
3456
|
+
});
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
} catch (error) {
|
|
3460
|
+
viewsValid = false;
|
|
3461
|
+
issues.push({
|
|
3462
|
+
type: "error",
|
|
3463
|
+
category: "views",
|
|
3464
|
+
message: `View validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3465
|
+
});
|
|
3466
|
+
}
|
|
3467
|
+
return viewsValid;
|
|
3468
|
+
}
|
|
3469
|
+
async function validateAttachmentIntegrity(db, issues) {
|
|
3470
|
+
let attachmentsValid = true;
|
|
3471
|
+
try {
|
|
3472
|
+
const allDocs = await db.allDocs({
|
|
3473
|
+
include_docs: true,
|
|
3474
|
+
limit: 10
|
|
3475
|
+
// Sample first 10 documents for performance
|
|
3476
|
+
});
|
|
3477
|
+
let attachmentCount = 0;
|
|
3478
|
+
let validAttachments = 0;
|
|
3479
|
+
for (const row of allDocs.rows) {
|
|
3480
|
+
const doc = row.doc;
|
|
3481
|
+
if (doc && doc._attachments) {
|
|
3482
|
+
for (const [attachmentName, _attachmentMeta] of Object.entries(doc._attachments)) {
|
|
3483
|
+
attachmentCount++;
|
|
3484
|
+
try {
|
|
3485
|
+
const attachment = await db.getAttachment(doc._id, attachmentName);
|
|
3486
|
+
if (attachment) {
|
|
3487
|
+
validAttachments++;
|
|
3488
|
+
}
|
|
3489
|
+
} catch (attachmentError) {
|
|
3490
|
+
attachmentsValid = false;
|
|
3491
|
+
issues.push({
|
|
3492
|
+
type: "error",
|
|
3493
|
+
category: "attachments",
|
|
3494
|
+
message: `Attachment not accessible: ${doc._id}/${attachmentName} - ${attachmentError}`
|
|
3495
|
+
});
|
|
3496
|
+
}
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
}
|
|
3500
|
+
if (attachmentCount === 0) {
|
|
3501
|
+
issues.push({
|
|
3502
|
+
type: "warning",
|
|
3503
|
+
category: "attachments",
|
|
3504
|
+
message: "No attachments found in sampled documents"
|
|
3505
|
+
});
|
|
3506
|
+
} else {
|
|
3507
|
+
logger.info(`Validated ${validAttachments}/${attachmentCount} sampled attachments`);
|
|
3508
|
+
}
|
|
3509
|
+
} catch (error) {
|
|
3510
|
+
attachmentsValid = false;
|
|
3511
|
+
issues.push({
|
|
3512
|
+
type: "error",
|
|
3513
|
+
category: "attachments",
|
|
3514
|
+
message: `Attachment validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3515
|
+
});
|
|
3516
|
+
}
|
|
3517
|
+
return attachmentsValid;
|
|
3518
|
+
}
|
|
3519
|
+
var nodeFS;
|
|
3520
|
+
var init_validation = __esm({
|
|
3521
|
+
"src/util/migrator/validation.ts"() {
|
|
3522
|
+
"use strict";
|
|
3523
|
+
init_logger();
|
|
3524
|
+
init_FileSystemAdapter();
|
|
3525
|
+
nodeFS = null;
|
|
3526
|
+
try {
|
|
3527
|
+
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3528
|
+
nodeFS = eval("require")("fs");
|
|
3529
|
+
nodeFS.promises = nodeFS.promises || eval("require")("fs").promises;
|
|
3530
|
+
}
|
|
3531
|
+
} catch {
|
|
3532
|
+
}
|
|
3533
|
+
}
|
|
3534
|
+
});
|
|
3535
|
+
|
|
3536
|
+
// src/util/migrator/StaticToCouchDBMigrator.ts
|
|
3537
|
+
var nodeFS2, nodePath, StaticToCouchDBMigrator;
|
|
3538
|
+
var init_StaticToCouchDBMigrator = __esm({
|
|
3539
|
+
"src/util/migrator/StaticToCouchDBMigrator.ts"() {
|
|
3540
|
+
"use strict";
|
|
3541
|
+
init_logger();
|
|
3542
|
+
init_types4();
|
|
3543
|
+
init_validation();
|
|
3544
|
+
init_FileSystemAdapter();
|
|
3545
|
+
nodeFS2 = null;
|
|
3546
|
+
nodePath = null;
|
|
3547
|
+
try {
|
|
3548
|
+
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3549
|
+
nodeFS2 = eval("require")("fs");
|
|
3550
|
+
nodePath = eval("require")("path");
|
|
3551
|
+
nodeFS2.promises = nodeFS2.promises || eval("require")("fs").promises;
|
|
3552
|
+
}
|
|
3553
|
+
} catch {
|
|
3554
|
+
}
|
|
3555
|
+
StaticToCouchDBMigrator = class {
|
|
3556
|
+
options;
|
|
3557
|
+
progressCallback;
|
|
3558
|
+
fs;
|
|
3559
|
+
constructor(options = {}, fileSystemAdapter) {
|
|
3560
|
+
this.options = {
|
|
3561
|
+
...DEFAULT_MIGRATION_OPTIONS,
|
|
3562
|
+
...options
|
|
3563
|
+
};
|
|
3564
|
+
this.fs = fileSystemAdapter;
|
|
3565
|
+
}
|
|
3566
|
+
/**
|
|
3567
|
+
* Set a progress callback to receive updates during migration
|
|
3568
|
+
*/
|
|
3569
|
+
setProgressCallback(callback) {
|
|
3570
|
+
this.progressCallback = callback;
|
|
3571
|
+
}
|
|
3572
|
+
/**
|
|
3573
|
+
* Migrate a static course to CouchDB
|
|
3574
|
+
*/
|
|
3575
|
+
async migrateCourse(staticPath, targetDB) {
|
|
3576
|
+
const startTime = Date.now();
|
|
3577
|
+
const result = {
|
|
3578
|
+
success: false,
|
|
3579
|
+
documentsRestored: 0,
|
|
3580
|
+
attachmentsRestored: 0,
|
|
3581
|
+
designDocsRestored: 0,
|
|
3582
|
+
courseConfigRestored: 0,
|
|
3583
|
+
errors: [],
|
|
3584
|
+
warnings: [],
|
|
3585
|
+
migrationTime: 0
|
|
3586
|
+
};
|
|
3587
|
+
try {
|
|
3588
|
+
logger.info(`Starting migration from ${staticPath} to CouchDB`);
|
|
3589
|
+
this.reportProgress("manifest", 0, 1, "Validating static course...");
|
|
3590
|
+
const validation = await validateStaticCourse(staticPath, this.fs);
|
|
3591
|
+
if (!validation.valid) {
|
|
3592
|
+
result.errors.push(...validation.errors);
|
|
3593
|
+
throw new Error(`Static course validation failed: ${validation.errors.join(", ")}`);
|
|
3594
|
+
}
|
|
3595
|
+
result.warnings.push(...validation.warnings);
|
|
3596
|
+
this.reportProgress("manifest", 1, 1, "Loading course manifest...");
|
|
3597
|
+
const manifest = await this.loadManifest(staticPath);
|
|
3598
|
+
logger.info(`Loaded manifest for course: ${manifest.courseId} (${manifest.courseName})`);
|
|
3599
|
+
this.reportProgress(
|
|
3600
|
+
"design_docs",
|
|
3601
|
+
0,
|
|
3602
|
+
manifest.designDocs.length,
|
|
3603
|
+
"Restoring design documents..."
|
|
3604
|
+
);
|
|
3605
|
+
const designDocResults = await this.restoreDesignDocuments(manifest.designDocs, targetDB);
|
|
3606
|
+
result.designDocsRestored = designDocResults.restored;
|
|
3607
|
+
result.errors.push(...designDocResults.errors);
|
|
3608
|
+
result.warnings.push(...designDocResults.warnings);
|
|
3609
|
+
this.reportProgress("course_config", 0, 1, "Restoring CourseConfig document...");
|
|
3610
|
+
const courseConfigResults = await this.restoreCourseConfig(manifest, targetDB);
|
|
3611
|
+
result.courseConfigRestored = courseConfigResults.restored;
|
|
3612
|
+
result.errors.push(...courseConfigResults.errors);
|
|
3613
|
+
result.warnings.push(...courseConfigResults.warnings);
|
|
3614
|
+
this.reportProgress("course_config", 1, 1, "CourseConfig document restored");
|
|
3615
|
+
const expectedCounts = this.calculateExpectedCounts(manifest);
|
|
3616
|
+
this.reportProgress(
|
|
3617
|
+
"documents",
|
|
3618
|
+
0,
|
|
3619
|
+
manifest.documentCount,
|
|
3620
|
+
"Aggregating documents from chunks..."
|
|
3621
|
+
);
|
|
3622
|
+
const documents = await this.aggregateDocuments(staticPath, manifest);
|
|
3623
|
+
const filteredDocuments = documents.filter((doc) => doc._id !== "CourseConfig");
|
|
3624
|
+
if (documents.length !== filteredDocuments.length) {
|
|
3625
|
+
result.warnings.push(
|
|
3626
|
+
`Filtered out ${documents.length - filteredDocuments.length} CourseConfig document(s) from chunks to prevent conflicts`
|
|
3627
|
+
);
|
|
3628
|
+
}
|
|
3629
|
+
this.reportProgress(
|
|
3630
|
+
"documents",
|
|
3631
|
+
filteredDocuments.length,
|
|
3632
|
+
manifest.documentCount,
|
|
3633
|
+
"Uploading documents to CouchDB..."
|
|
3634
|
+
);
|
|
3635
|
+
const docResults = await this.uploadDocuments(filteredDocuments, targetDB);
|
|
3636
|
+
result.documentsRestored = docResults.restored;
|
|
3637
|
+
result.errors.push(...docResults.errors);
|
|
3638
|
+
result.warnings.push(...docResults.warnings);
|
|
3639
|
+
const docsWithAttachments = documents.filter(
|
|
3640
|
+
(doc) => doc._attachments && Object.keys(doc._attachments).length > 0
|
|
3641
|
+
);
|
|
3642
|
+
this.reportProgress("attachments", 0, docsWithAttachments.length, "Uploading attachments...");
|
|
3643
|
+
const attachmentResults = await this.uploadAttachments(
|
|
3644
|
+
staticPath,
|
|
3645
|
+
docsWithAttachments,
|
|
3646
|
+
targetDB
|
|
3647
|
+
);
|
|
3648
|
+
result.attachmentsRestored = attachmentResults.restored;
|
|
3649
|
+
result.errors.push(...attachmentResults.errors);
|
|
3650
|
+
result.warnings.push(...attachmentResults.warnings);
|
|
3651
|
+
if (this.options.validateRoundTrip) {
|
|
3652
|
+
this.reportProgress("validation", 0, 1, "Validating migration...");
|
|
3653
|
+
const validationResult = await validateMigration(targetDB, expectedCounts, manifest);
|
|
3654
|
+
if (!validationResult.valid) {
|
|
3655
|
+
result.warnings.push("Migration validation found issues");
|
|
3656
|
+
validationResult.issues.forEach((issue) => {
|
|
3657
|
+
if (issue.type === "error") {
|
|
3658
|
+
result.errors.push(`Validation: ${issue.message}`);
|
|
3659
|
+
} else {
|
|
3660
|
+
result.warnings.push(`Validation: ${issue.message}`);
|
|
3661
|
+
}
|
|
3662
|
+
});
|
|
3663
|
+
}
|
|
3664
|
+
this.reportProgress("validation", 1, 1, "Migration validation completed");
|
|
3665
|
+
}
|
|
3666
|
+
result.success = result.errors.length === 0;
|
|
3667
|
+
result.migrationTime = Date.now() - startTime;
|
|
3668
|
+
logger.info(`Migration completed in ${result.migrationTime}ms`);
|
|
3669
|
+
logger.info(`Documents restored: ${result.documentsRestored}`);
|
|
3670
|
+
logger.info(`Attachments restored: ${result.attachmentsRestored}`);
|
|
3671
|
+
logger.info(`Design docs restored: ${result.designDocsRestored}`);
|
|
3672
|
+
logger.info(`CourseConfig restored: ${result.courseConfigRestored}`);
|
|
3673
|
+
if (result.errors.length > 0) {
|
|
3674
|
+
logger.error(`Migration completed with ${result.errors.length} errors`);
|
|
3675
|
+
}
|
|
3676
|
+
if (result.warnings.length > 0) {
|
|
3677
|
+
logger.warn(`Migration completed with ${result.warnings.length} warnings`);
|
|
3678
|
+
}
|
|
3679
|
+
} catch (error) {
|
|
3680
|
+
result.success = false;
|
|
3681
|
+
result.migrationTime = Date.now() - startTime;
|
|
3682
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3683
|
+
result.errors.push(`Migration failed: ${errorMessage}`);
|
|
3684
|
+
logger.error("Migration failed:", error);
|
|
3685
|
+
if (this.options.cleanupOnFailure) {
|
|
3686
|
+
try {
|
|
3687
|
+
await this.cleanupFailedMigration(targetDB);
|
|
3688
|
+
} catch (cleanupError) {
|
|
3689
|
+
logger.error("Failed to cleanup after migration failure:", cleanupError);
|
|
3690
|
+
result.warnings.push("Failed to cleanup after migration failure");
|
|
3691
|
+
}
|
|
3692
|
+
}
|
|
3693
|
+
}
|
|
3694
|
+
return result;
|
|
3695
|
+
}
|
|
3696
|
+
/**
|
|
3697
|
+
* Load and parse the manifest file
|
|
3698
|
+
*/
|
|
3699
|
+
async loadManifest(staticPath) {
|
|
3700
|
+
try {
|
|
3701
|
+
let manifestContent;
|
|
3702
|
+
let manifestPath;
|
|
3703
|
+
if (this.fs) {
|
|
3704
|
+
manifestPath = this.fs.joinPath(staticPath, "manifest.json");
|
|
3705
|
+
manifestContent = await this.fs.readFile(manifestPath);
|
|
3706
|
+
} else {
|
|
3707
|
+
manifestPath = nodeFS2 && nodePath ? nodePath.join(staticPath, "manifest.json") : `${staticPath}/manifest.json`;
|
|
3708
|
+
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
3709
|
+
manifestContent = await nodeFS2.promises.readFile(manifestPath, "utf8");
|
|
3710
|
+
} else {
|
|
3711
|
+
const response = await fetch(manifestPath);
|
|
3712
|
+
if (!response.ok) {
|
|
3713
|
+
throw new Error(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
|
|
3714
|
+
}
|
|
3715
|
+
manifestContent = await response.text();
|
|
3716
|
+
}
|
|
3717
|
+
}
|
|
3718
|
+
const manifest = JSON.parse(manifestContent);
|
|
3719
|
+
if (!manifest.version || !manifest.courseId || !manifest.chunks) {
|
|
3720
|
+
throw new Error("Invalid manifest structure");
|
|
3721
|
+
}
|
|
3722
|
+
return manifest;
|
|
3723
|
+
} catch (error) {
|
|
3724
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load manifest: ${error instanceof Error ? error.message : String(error)}`;
|
|
3725
|
+
throw new Error(errorMessage);
|
|
3726
|
+
}
|
|
3727
|
+
}
|
|
3728
|
+
/**
|
|
3729
|
+
* Restore design documents to CouchDB
|
|
3730
|
+
*/
|
|
3731
|
+
async restoreDesignDocuments(designDocs, db) {
|
|
3732
|
+
const result = { restored: 0, errors: [], warnings: [] };
|
|
3733
|
+
for (let i = 0; i < designDocs.length; i++) {
|
|
3734
|
+
const designDoc = designDocs[i];
|
|
3735
|
+
this.reportProgress("design_docs", i, designDocs.length, `Restoring ${designDoc._id}...`);
|
|
3736
|
+
try {
|
|
3737
|
+
let existingDoc;
|
|
3738
|
+
try {
|
|
3739
|
+
existingDoc = await db.get(designDoc._id);
|
|
3740
|
+
} catch {
|
|
3741
|
+
}
|
|
3742
|
+
const docToInsert = {
|
|
3743
|
+
_id: designDoc._id,
|
|
3744
|
+
views: designDoc.views
|
|
3745
|
+
};
|
|
3746
|
+
if (existingDoc) {
|
|
3747
|
+
docToInsert._rev = existingDoc._rev;
|
|
3748
|
+
logger.debug(`Updating existing design document: ${designDoc._id}`);
|
|
3749
|
+
} else {
|
|
3750
|
+
logger.debug(`Creating new design document: ${designDoc._id}`);
|
|
3751
|
+
}
|
|
3752
|
+
await db.put(docToInsert);
|
|
3753
|
+
result.restored++;
|
|
3754
|
+
} catch (error) {
|
|
3755
|
+
const errorMessage = `Failed to restore design document ${designDoc._id}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3756
|
+
result.errors.push(errorMessage);
|
|
3757
|
+
logger.error(errorMessage);
|
|
3758
|
+
}
|
|
3759
|
+
}
|
|
3760
|
+
this.reportProgress(
|
|
3761
|
+
"design_docs",
|
|
3762
|
+
designDocs.length,
|
|
3763
|
+
designDocs.length,
|
|
3764
|
+
`Restored ${result.restored} design documents`
|
|
3765
|
+
);
|
|
3766
|
+
return result;
|
|
3767
|
+
}
|
|
3768
|
+
/**
|
|
3769
|
+
* Aggregate documents from all chunks
|
|
3770
|
+
*/
|
|
3771
|
+
async aggregateDocuments(staticPath, manifest) {
|
|
3772
|
+
const allDocuments = [];
|
|
3773
|
+
const documentMap = /* @__PURE__ */ new Map();
|
|
3774
|
+
for (let i = 0; i < manifest.chunks.length; i++) {
|
|
3775
|
+
const chunk = manifest.chunks[i];
|
|
3776
|
+
this.reportProgress(
|
|
3777
|
+
"documents",
|
|
3778
|
+
allDocuments.length,
|
|
3779
|
+
manifest.documentCount,
|
|
3780
|
+
`Loading chunk ${chunk.id}...`
|
|
3781
|
+
);
|
|
3782
|
+
try {
|
|
3783
|
+
const documents = await this.loadChunk(staticPath, chunk);
|
|
3784
|
+
for (const doc of documents) {
|
|
3785
|
+
if (!doc._id) {
|
|
3786
|
+
logger.warn(`Document without _id found in chunk ${chunk.id}, skipping`);
|
|
3787
|
+
continue;
|
|
3788
|
+
}
|
|
3789
|
+
if (documentMap.has(doc._id)) {
|
|
3790
|
+
logger.warn(`Duplicate document ID found: ${doc._id}, using latest version`);
|
|
3791
|
+
}
|
|
3792
|
+
documentMap.set(doc._id, doc);
|
|
3793
|
+
}
|
|
3794
|
+
} catch (error) {
|
|
3795
|
+
throw new Error(
|
|
3796
|
+
`Failed to load chunk ${chunk.id}: ${error instanceof Error ? error.message : String(error)}`
|
|
3797
|
+
);
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
allDocuments.push(...documentMap.values());
|
|
3801
|
+
logger.info(
|
|
3802
|
+
`Aggregated ${allDocuments.length} unique documents from ${manifest.chunks.length} chunks`
|
|
3803
|
+
);
|
|
3804
|
+
return allDocuments;
|
|
3805
|
+
}
|
|
3806
|
+
/**
|
|
3807
|
+
* Load documents from a single chunk file
|
|
3808
|
+
*/
|
|
3809
|
+
async loadChunk(staticPath, chunk) {
|
|
3810
|
+
try {
|
|
3811
|
+
let chunkContent;
|
|
3812
|
+
let chunkPath;
|
|
3813
|
+
if (this.fs) {
|
|
3814
|
+
chunkPath = this.fs.joinPath(staticPath, chunk.path);
|
|
3815
|
+
chunkContent = await this.fs.readFile(chunkPath);
|
|
3816
|
+
} else {
|
|
3817
|
+
chunkPath = nodeFS2 && nodePath ? nodePath.join(staticPath, chunk.path) : `${staticPath}/${chunk.path}`;
|
|
3818
|
+
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
3819
|
+
chunkContent = await nodeFS2.promises.readFile(chunkPath, "utf8");
|
|
3820
|
+
} else {
|
|
3821
|
+
const response = await fetch(chunkPath);
|
|
3822
|
+
if (!response.ok) {
|
|
3823
|
+
throw new Error(`Failed to fetch chunk: ${response.status} ${response.statusText}`);
|
|
3824
|
+
}
|
|
3825
|
+
chunkContent = await response.text();
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
const documents = JSON.parse(chunkContent);
|
|
3829
|
+
if (!Array.isArray(documents)) {
|
|
3830
|
+
throw new Error("Chunk file does not contain an array of documents");
|
|
3831
|
+
}
|
|
3832
|
+
return documents;
|
|
3833
|
+
} catch (error) {
|
|
3834
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load chunk: ${error instanceof Error ? error.message : String(error)}`;
|
|
3835
|
+
throw new Error(errorMessage);
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
/**
|
|
3839
|
+
* Upload documents to CouchDB in batches
|
|
3840
|
+
*/
|
|
3841
|
+
async uploadDocuments(documents, db) {
|
|
3842
|
+
const result = { restored: 0, errors: [], warnings: [] };
|
|
3843
|
+
const batchSize = this.options.chunkBatchSize;
|
|
3844
|
+
for (let i = 0; i < documents.length; i += batchSize) {
|
|
3845
|
+
const batch = documents.slice(i, i + batchSize);
|
|
3846
|
+
this.reportProgress(
|
|
3847
|
+
"documents",
|
|
3848
|
+
i,
|
|
3849
|
+
documents.length,
|
|
3850
|
+
`Uploading batch ${Math.floor(i / batchSize) + 1}...`
|
|
3851
|
+
);
|
|
3852
|
+
try {
|
|
3853
|
+
const docsToInsert = batch.map((doc) => {
|
|
3854
|
+
const cleanDoc = { ...doc };
|
|
3855
|
+
delete cleanDoc._rev;
|
|
3856
|
+
delete cleanDoc._attachments;
|
|
3857
|
+
return cleanDoc;
|
|
3858
|
+
});
|
|
3859
|
+
const bulkResult = await db.bulkDocs(docsToInsert);
|
|
3860
|
+
for (let j = 0; j < bulkResult.length; j++) {
|
|
3861
|
+
const docResult = bulkResult[j];
|
|
3862
|
+
const originalDoc = batch[j];
|
|
3863
|
+
if ("error" in docResult) {
|
|
3864
|
+
const errorMessage = `Failed to upload document ${originalDoc._id}: ${docResult.error} - ${docResult.reason}`;
|
|
3865
|
+
result.errors.push(errorMessage);
|
|
3866
|
+
logger.error(errorMessage);
|
|
3867
|
+
} else {
|
|
3868
|
+
result.restored++;
|
|
3869
|
+
}
|
|
3870
|
+
}
|
|
3871
|
+
} catch (error) {
|
|
3872
|
+
let errorMessage;
|
|
3873
|
+
if (error instanceof Error) {
|
|
3874
|
+
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
3875
|
+
} else if (error && typeof error === "object" && "message" in error) {
|
|
3876
|
+
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
3877
|
+
} else {
|
|
3878
|
+
errorMessage = `Failed to upload document batch starting at index ${i}: ${JSON.stringify(error)}`;
|
|
3879
|
+
}
|
|
3880
|
+
result.errors.push(errorMessage);
|
|
3881
|
+
logger.error(errorMessage);
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
this.reportProgress(
|
|
3885
|
+
"documents",
|
|
3886
|
+
documents.length,
|
|
3887
|
+
documents.length,
|
|
3888
|
+
`Uploaded ${result.restored} documents`
|
|
3889
|
+
);
|
|
3890
|
+
return result;
|
|
3891
|
+
}
|
|
3892
|
+
/**
|
|
3893
|
+
* Upload attachments from filesystem to CouchDB
|
|
3894
|
+
*/
|
|
3895
|
+
async uploadAttachments(staticPath, documents, db) {
|
|
3896
|
+
const result = { restored: 0, errors: [], warnings: [] };
|
|
3897
|
+
let processedDocs = 0;
|
|
3898
|
+
for (const doc of documents) {
|
|
3899
|
+
this.reportProgress(
|
|
3900
|
+
"attachments",
|
|
3901
|
+
processedDocs,
|
|
3902
|
+
documents.length,
|
|
3903
|
+
`Processing attachments for ${doc._id}...`
|
|
3904
|
+
);
|
|
3905
|
+
processedDocs++;
|
|
3906
|
+
if (!doc._attachments) {
|
|
3907
|
+
continue;
|
|
3908
|
+
}
|
|
3909
|
+
for (const [attachmentName, attachmentMeta] of Object.entries(doc._attachments)) {
|
|
3910
|
+
try {
|
|
3911
|
+
const uploadResult = await this.uploadSingleAttachment(
|
|
3912
|
+
staticPath,
|
|
3913
|
+
doc._id,
|
|
3914
|
+
attachmentName,
|
|
3915
|
+
attachmentMeta,
|
|
3916
|
+
db
|
|
3917
|
+
);
|
|
3918
|
+
if (uploadResult.success) {
|
|
3919
|
+
result.restored++;
|
|
3920
|
+
} else {
|
|
3921
|
+
result.errors.push(uploadResult.error || "Unknown attachment upload error");
|
|
3922
|
+
}
|
|
3923
|
+
} catch (error) {
|
|
3924
|
+
const errorMessage = `Failed to upload attachment ${doc._id}/${attachmentName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
3925
|
+
result.errors.push(errorMessage);
|
|
3926
|
+
logger.error(errorMessage);
|
|
3927
|
+
}
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
this.reportProgress(
|
|
3931
|
+
"attachments",
|
|
3932
|
+
documents.length,
|
|
3933
|
+
documents.length,
|
|
3934
|
+
`Uploaded ${result.restored} attachments`
|
|
3935
|
+
);
|
|
3936
|
+
return result;
|
|
3937
|
+
}
|
|
3938
|
+
/**
|
|
3939
|
+
* Upload a single attachment file
|
|
3940
|
+
*/
|
|
3941
|
+
async uploadSingleAttachment(staticPath, docId, attachmentName, attachmentMeta, db) {
|
|
3942
|
+
const result = {
|
|
3943
|
+
success: false,
|
|
3944
|
+
attachmentName,
|
|
3945
|
+
docId
|
|
3946
|
+
};
|
|
3947
|
+
try {
|
|
3948
|
+
if (!attachmentMeta.path) {
|
|
3949
|
+
result.error = "Attachment metadata missing file path";
|
|
3950
|
+
return result;
|
|
3951
|
+
}
|
|
3952
|
+
let attachmentData;
|
|
3953
|
+
let attachmentPath;
|
|
3954
|
+
if (this.fs) {
|
|
3955
|
+
attachmentPath = this.fs.joinPath(staticPath, attachmentMeta.path);
|
|
3956
|
+
attachmentData = await this.fs.readBinary(attachmentPath);
|
|
3957
|
+
} else {
|
|
3958
|
+
attachmentPath = nodeFS2 && nodePath ? nodePath.join(staticPath, attachmentMeta.path) : `${staticPath}/${attachmentMeta.path}`;
|
|
3959
|
+
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
3960
|
+
attachmentData = await nodeFS2.promises.readFile(attachmentPath);
|
|
3961
|
+
} else {
|
|
3962
|
+
const response = await fetch(attachmentPath);
|
|
3963
|
+
if (!response.ok) {
|
|
3964
|
+
result.error = `Failed to fetch attachment: ${response.status} ${response.statusText}`;
|
|
3965
|
+
return result;
|
|
3966
|
+
}
|
|
3967
|
+
attachmentData = await response.arrayBuffer();
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
const doc = await db.get(docId);
|
|
3971
|
+
await db.putAttachment(
|
|
3972
|
+
docId,
|
|
3973
|
+
attachmentName,
|
|
3974
|
+
doc._rev,
|
|
3975
|
+
attachmentData,
|
|
3976
|
+
// PouchDB accepts both ArrayBuffer and Buffer
|
|
3977
|
+
attachmentMeta.content_type
|
|
3978
|
+
);
|
|
3979
|
+
result.success = true;
|
|
3980
|
+
} catch (error) {
|
|
3981
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
3982
|
+
}
|
|
3983
|
+
return result;
|
|
3984
|
+
}
|
|
3985
|
+
/**
|
|
3986
|
+
* Restore CourseConfig document from manifest
|
|
3987
|
+
*/
|
|
3988
|
+
async restoreCourseConfig(manifest, targetDB) {
|
|
3989
|
+
const results = {
|
|
3990
|
+
restored: 0,
|
|
3991
|
+
errors: [],
|
|
3992
|
+
warnings: []
|
|
3993
|
+
};
|
|
3994
|
+
try {
|
|
3995
|
+
if (!manifest.courseConfig) {
|
|
3996
|
+
results.warnings.push(
|
|
3997
|
+
"No courseConfig found in manifest, skipping CourseConfig document creation"
|
|
3998
|
+
);
|
|
3999
|
+
return results;
|
|
4000
|
+
}
|
|
4001
|
+
const courseConfigDoc = {
|
|
4002
|
+
_id: "CourseConfig",
|
|
4003
|
+
...manifest.courseConfig,
|
|
4004
|
+
courseID: manifest.courseId
|
|
4005
|
+
};
|
|
4006
|
+
delete courseConfigDoc._rev;
|
|
4007
|
+
await targetDB.put(courseConfigDoc);
|
|
4008
|
+
results.restored = 1;
|
|
4009
|
+
logger.info(`CourseConfig document created for course: ${manifest.courseId}`);
|
|
4010
|
+
} catch (error) {
|
|
4011
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
4012
|
+
results.errors.push(`Failed to restore CourseConfig: ${errorMessage}`);
|
|
4013
|
+
logger.error("CourseConfig restoration failed:", error);
|
|
4014
|
+
}
|
|
4015
|
+
return results;
|
|
4016
|
+
}
|
|
4017
|
+
/**
|
|
4018
|
+
* Calculate expected document counts from manifest
|
|
4019
|
+
*/
|
|
4020
|
+
calculateExpectedCounts(manifest) {
|
|
4021
|
+
const counts = {};
|
|
4022
|
+
for (const chunk of manifest.chunks) {
|
|
4023
|
+
counts[chunk.docType] = (counts[chunk.docType] || 0) + chunk.documentCount;
|
|
4024
|
+
}
|
|
4025
|
+
if (manifest.designDocs.length > 0) {
|
|
4026
|
+
counts["_design"] = manifest.designDocs.length;
|
|
4027
|
+
}
|
|
4028
|
+
return counts;
|
|
4029
|
+
}
|
|
4030
|
+
/**
|
|
4031
|
+
* Clean up database after failed migration
|
|
4032
|
+
*/
|
|
4033
|
+
async cleanupFailedMigration(db) {
|
|
4034
|
+
logger.info("Cleaning up failed migration...");
|
|
4035
|
+
try {
|
|
4036
|
+
const allDocs = await db.allDocs();
|
|
4037
|
+
const docsToDelete = allDocs.rows.map((row) => ({
|
|
4038
|
+
_id: row.id,
|
|
4039
|
+
_rev: row.value.rev,
|
|
4040
|
+
_deleted: true
|
|
4041
|
+
}));
|
|
4042
|
+
if (docsToDelete.length > 0) {
|
|
4043
|
+
await db.bulkDocs(docsToDelete);
|
|
4044
|
+
logger.info(`Cleaned up ${docsToDelete.length} documents from failed migration`);
|
|
4045
|
+
}
|
|
4046
|
+
} catch (error) {
|
|
4047
|
+
logger.error("Failed to cleanup documents:", error);
|
|
4048
|
+
throw error;
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
/**
|
|
4052
|
+
* Report progress to callback if available
|
|
4053
|
+
*/
|
|
4054
|
+
reportProgress(phase, current, total, message) {
|
|
4055
|
+
if (this.progressCallback) {
|
|
4056
|
+
this.progressCallback({
|
|
4057
|
+
phase,
|
|
4058
|
+
current,
|
|
4059
|
+
total,
|
|
4060
|
+
message
|
|
4061
|
+
});
|
|
4062
|
+
}
|
|
4063
|
+
}
|
|
4064
|
+
/**
|
|
4065
|
+
* Check if a path is a local file path (vs URL)
|
|
4066
|
+
*/
|
|
4067
|
+
isLocalPath(path2) {
|
|
4068
|
+
return !path2.startsWith("http://") && !path2.startsWith("https://");
|
|
4069
|
+
}
|
|
4070
|
+
};
|
|
4071
|
+
}
|
|
4072
|
+
});
|
|
4073
|
+
|
|
4074
|
+
// src/util/migrator/index.ts
|
|
4075
|
+
var init_migrator = __esm({
|
|
4076
|
+
"src/util/migrator/index.ts"() {
|
|
4077
|
+
"use strict";
|
|
4078
|
+
init_StaticToCouchDBMigrator();
|
|
4079
|
+
init_validation();
|
|
4080
|
+
init_FileSystemAdapter();
|
|
4081
|
+
}
|
|
4082
|
+
});
|
|
4083
|
+
|
|
4084
|
+
// src/util/dataDirectory.ts
|
|
4085
|
+
function getAppDataDirectory() {
|
|
4086
|
+
if (ENV.LOCAL_STORAGE_PREFIX) {
|
|
4087
|
+
return path.join(os.homedir(), `.tuilder`, ENV.LOCAL_STORAGE_PREFIX);
|
|
4088
|
+
} else {
|
|
4089
|
+
return path.join(os.homedir(), ".tuilder");
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
function getDbPath(dbName) {
|
|
4093
|
+
return path.join(getAppDataDirectory(), dbName);
|
|
4094
|
+
}
|
|
4095
|
+
var path, os;
|
|
4096
|
+
var init_dataDirectory = __esm({
|
|
4097
|
+
"src/util/dataDirectory.ts"() {
|
|
4098
|
+
"use strict";
|
|
4099
|
+
path = __toESM(require("path"), 1);
|
|
4100
|
+
os = __toESM(require("os"), 1);
|
|
4101
|
+
init_logger();
|
|
4102
|
+
init_factory();
|
|
4103
|
+
}
|
|
4104
|
+
});
|
|
4105
|
+
|
|
4106
|
+
// src/util/index.ts
|
|
4107
|
+
var init_util2 = __esm({
|
|
4108
|
+
"src/util/index.ts"() {
|
|
4109
|
+
"use strict";
|
|
4110
|
+
init_Loggable();
|
|
4111
|
+
init_packer();
|
|
4112
|
+
init_migrator();
|
|
4113
|
+
init_dataDirectory();
|
|
4114
|
+
}
|
|
4115
|
+
});
|
|
4116
|
+
|
|
4117
|
+
// src/study/SourceMixer.ts
|
|
4118
|
+
var init_SourceMixer = __esm({
|
|
4119
|
+
"src/study/SourceMixer.ts"() {
|
|
4120
|
+
"use strict";
|
|
4121
|
+
}
|
|
4122
|
+
});
|
|
4123
|
+
|
|
4124
|
+
// src/study/MixerDebugger.ts
|
|
4125
|
+
function printMixerSummary(run) {
|
|
4126
|
+
console.group(`\u{1F3A8} Mixer Run: ${run.mixerType}`);
|
|
4127
|
+
logger.info(`Run ID: ${run.runId}`);
|
|
4128
|
+
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
4129
|
+
logger.info(
|
|
4130
|
+
`Config: limit=${run.requestedLimit}${run.quotaPerSource ? `, quota/source=${run.quotaPerSource}` : ""}`
|
|
4131
|
+
);
|
|
4132
|
+
console.group(`\u{1F4E5} Input: ${run.sourceSummaries.length} sources`);
|
|
4133
|
+
for (const src of run.sourceSummaries) {
|
|
4134
|
+
logger.info(
|
|
4135
|
+
` ${src.sourceName || src.sourceId}: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`
|
|
4136
|
+
);
|
|
4137
|
+
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}], avg: ${src.avgScore.toFixed(2)}`);
|
|
4138
|
+
}
|
|
4139
|
+
console.groupEnd();
|
|
4140
|
+
console.group(`\u{1F4E4} Output: ${run.finalCount} cards selected (${run.reviewsSelected} reviews, ${run.newSelected} new)`);
|
|
4141
|
+
for (const breakdown of run.sourceBreakdowns) {
|
|
4142
|
+
const name = breakdown.sourceName || breakdown.sourceId;
|
|
4143
|
+
logger.info(
|
|
4144
|
+
` ${name}: ${breakdown.totalSelected} selected (${breakdown.reviewsSelected} reviews, ${breakdown.newSelected} new) - ${breakdown.selectionRate.toFixed(1)}% selection rate`
|
|
4145
|
+
);
|
|
4146
|
+
}
|
|
4147
|
+
console.groupEnd();
|
|
4148
|
+
console.groupEnd();
|
|
4149
|
+
}
|
|
4150
|
+
function mountMixerDebugger() {
|
|
4151
|
+
if (typeof window === "undefined") return;
|
|
4152
|
+
const win = window;
|
|
4153
|
+
win.skuilder = win.skuilder || {};
|
|
4154
|
+
win.skuilder.mixer = mixerDebugAPI;
|
|
4155
|
+
}
|
|
4156
|
+
var runHistory2, mixerDebugAPI;
|
|
4157
|
+
var init_MixerDebugger = __esm({
|
|
4158
|
+
"src/study/MixerDebugger.ts"() {
|
|
4159
|
+
"use strict";
|
|
4160
|
+
init_logger();
|
|
4161
|
+
init_navigators();
|
|
4162
|
+
runHistory2 = [];
|
|
4163
|
+
mixerDebugAPI = {
|
|
4164
|
+
/**
|
|
4165
|
+
* Get raw run history for programmatic access.
|
|
4166
|
+
*/
|
|
4167
|
+
get runs() {
|
|
4168
|
+
return [...runHistory2];
|
|
4169
|
+
},
|
|
4170
|
+
/**
|
|
4171
|
+
* Show summary of a specific mixer run.
|
|
4172
|
+
*/
|
|
4173
|
+
showRun(idOrIndex = 0) {
|
|
4174
|
+
if (runHistory2.length === 0) {
|
|
4175
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4176
|
+
return;
|
|
4177
|
+
}
|
|
4178
|
+
let run;
|
|
4179
|
+
if (typeof idOrIndex === "number") {
|
|
4180
|
+
run = runHistory2[idOrIndex];
|
|
4181
|
+
if (!run) {
|
|
4182
|
+
logger.info(`[Mixer Debug] No run found at index ${idOrIndex}. History length: ${runHistory2.length}`);
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
} else {
|
|
4186
|
+
run = runHistory2.find((r) => r.runId.endsWith(idOrIndex));
|
|
4187
|
+
if (!run) {
|
|
4188
|
+
logger.info(`[Mixer Debug] No run found matching ID '${idOrIndex}'.`);
|
|
4189
|
+
return;
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
printMixerSummary(run);
|
|
4193
|
+
},
|
|
4194
|
+
/**
|
|
4195
|
+
* Show summary of the last mixer run.
|
|
4196
|
+
*/
|
|
4197
|
+
showLastMix() {
|
|
4198
|
+
this.showRun(0);
|
|
4199
|
+
},
|
|
4200
|
+
/**
|
|
4201
|
+
* Explain source balance in the last run.
|
|
4202
|
+
*/
|
|
4203
|
+
explainSourceBalance() {
|
|
4204
|
+
if (runHistory2.length === 0) {
|
|
4205
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4206
|
+
return;
|
|
4207
|
+
}
|
|
4208
|
+
const run = runHistory2[0];
|
|
4209
|
+
console.group("\u2696\uFE0F Source Balance Analysis");
|
|
4210
|
+
logger.info(`Mixer: ${run.mixerType}`);
|
|
4211
|
+
logger.info(`Requested limit: ${run.requestedLimit}`);
|
|
4212
|
+
if (run.quotaPerSource) {
|
|
4213
|
+
logger.info(`Quota per source: ${run.quotaPerSource}`);
|
|
4214
|
+
}
|
|
4215
|
+
console.group("Input Distribution:");
|
|
4216
|
+
for (const src of run.sourceSummaries) {
|
|
4217
|
+
const name = src.sourceName || src.sourceId;
|
|
4218
|
+
logger.info(`${name}:`);
|
|
4219
|
+
logger.info(` Provided: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`);
|
|
4220
|
+
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}]`);
|
|
4221
|
+
}
|
|
4222
|
+
console.groupEnd();
|
|
4223
|
+
console.group("Selection Results:");
|
|
4224
|
+
for (const breakdown of run.sourceBreakdowns) {
|
|
4225
|
+
const name = breakdown.sourceName || breakdown.sourceId;
|
|
4226
|
+
logger.info(`${name}:`);
|
|
4227
|
+
logger.info(
|
|
4228
|
+
` Selected: ${breakdown.totalSelected}/${breakdown.reviewsProvided + breakdown.newProvided} (${breakdown.selectionRate.toFixed(1)}%)`
|
|
4229
|
+
);
|
|
4230
|
+
logger.info(` Reviews: ${breakdown.reviewsSelected}/${breakdown.reviewsProvided}`);
|
|
4231
|
+
logger.info(` New: ${breakdown.newSelected}/${breakdown.newProvided}`);
|
|
4232
|
+
if (breakdown.reviewsProvided > 0 && breakdown.reviewsSelected === 0) {
|
|
4233
|
+
logger.info(` \u26A0\uFE0F Had reviews but none selected!`);
|
|
4234
|
+
}
|
|
4235
|
+
if (breakdown.totalSelected === 0 && breakdown.reviewsProvided + breakdown.newProvided > 0) {
|
|
4236
|
+
logger.info(` \u26A0\uFE0F Had cards but none selected!`);
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
console.groupEnd();
|
|
4240
|
+
const selectionRates = run.sourceBreakdowns.map((b) => b.selectionRate);
|
|
4241
|
+
const avgRate = selectionRates.reduce((a, b) => a + b, 0) / selectionRates.length;
|
|
4242
|
+
const maxDeviation = Math.max(...selectionRates.map((r) => Math.abs(r - avgRate)));
|
|
4243
|
+
if (maxDeviation > 20) {
|
|
4244
|
+
logger.info(`
|
|
4245
|
+
\u26A0\uFE0F Significant imbalance detected (max deviation: ${maxDeviation.toFixed(1)}%)`);
|
|
4246
|
+
logger.info("Possible causes:");
|
|
4247
|
+
logger.info(" - Score range differences between sources");
|
|
4248
|
+
logger.info(" - One source has much better quality cards");
|
|
4249
|
+
logger.info(" - Different card availability (reviews vs new)");
|
|
4250
|
+
}
|
|
4251
|
+
console.groupEnd();
|
|
4252
|
+
},
|
|
4253
|
+
/**
|
|
4254
|
+
* Compare score distributions across sources.
|
|
4255
|
+
*/
|
|
4256
|
+
compareScores() {
|
|
4257
|
+
if (runHistory2.length === 0) {
|
|
4258
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4259
|
+
return;
|
|
4260
|
+
}
|
|
4261
|
+
const run = runHistory2[0];
|
|
4262
|
+
console.group("\u{1F4CA} Score Distribution Comparison");
|
|
4263
|
+
console.table(
|
|
4264
|
+
run.sourceSummaries.map((src) => ({
|
|
4265
|
+
source: src.sourceName || src.sourceId,
|
|
4266
|
+
cards: src.totalCards,
|
|
4267
|
+
min: src.bottomScore.toFixed(3),
|
|
4268
|
+
max: src.topScore.toFixed(3),
|
|
4269
|
+
avg: src.avgScore.toFixed(3),
|
|
4270
|
+
range: (src.topScore - src.bottomScore).toFixed(3)
|
|
4271
|
+
}))
|
|
4272
|
+
);
|
|
4273
|
+
const ranges = run.sourceSummaries.map((s) => s.topScore - s.bottomScore);
|
|
4274
|
+
const avgScores = run.sourceSummaries.map((s) => s.avgScore);
|
|
4275
|
+
const rangeDiff = Math.max(...ranges) - Math.min(...ranges);
|
|
4276
|
+
const avgDiff = Math.max(...avgScores) - Math.min(...avgScores);
|
|
4277
|
+
if (rangeDiff > 0.3 || avgDiff > 0.2) {
|
|
4278
|
+
logger.info("\n\u26A0\uFE0F Significant score distribution differences detected");
|
|
4279
|
+
logger.info(
|
|
4280
|
+
"This may cause one source to dominate selection if using global sorting (not quota-based)"
|
|
4281
|
+
);
|
|
4282
|
+
}
|
|
4283
|
+
console.groupEnd();
|
|
4284
|
+
},
|
|
4285
|
+
/**
|
|
4286
|
+
* Show detailed information for a specific card.
|
|
4287
|
+
*/
|
|
4288
|
+
showCard(cardId) {
|
|
4289
|
+
for (const run of runHistory2) {
|
|
4290
|
+
const card = run.cards.find((c) => c.cardId === cardId);
|
|
4291
|
+
if (card) {
|
|
4292
|
+
const source = run.sourceSummaries.find((s) => s.sourceIndex === card.sourceIndex);
|
|
4293
|
+
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
4294
|
+
logger.info(`Course: ${card.courseId}`);
|
|
4295
|
+
logger.info(`Source: ${source?.sourceName || source?.sourceId || "unknown"}`);
|
|
4296
|
+
logger.info(`Origin: ${card.origin}`);
|
|
4297
|
+
logger.info(`Score: ${card.score.toFixed(3)}`);
|
|
4298
|
+
if (card.rankInSource) {
|
|
4299
|
+
logger.info(`Rank in source: #${card.rankInSource}`);
|
|
4300
|
+
}
|
|
4301
|
+
if (card.rankInMix) {
|
|
4302
|
+
logger.info(`Rank in mixed results: #${card.rankInMix}`);
|
|
4303
|
+
}
|
|
4304
|
+
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
4305
|
+
if (!card.selected && card.rankInSource) {
|
|
4306
|
+
logger.info("\nWhy not selected:");
|
|
4307
|
+
if (run.quotaPerSource && card.rankInSource > run.quotaPerSource) {
|
|
4308
|
+
logger.info(` - Ranked #${card.rankInSource} in source, but quota was ${run.quotaPerSource}`);
|
|
4309
|
+
}
|
|
4310
|
+
logger.info(" - Check score compared to selected cards using .showRun()");
|
|
4311
|
+
}
|
|
4312
|
+
console.groupEnd();
|
|
4313
|
+
return;
|
|
4314
|
+
}
|
|
4315
|
+
}
|
|
4316
|
+
logger.info(`[Mixer Debug] Card '${cardId}' not found in recent runs.`);
|
|
4317
|
+
},
|
|
4318
|
+
/**
|
|
4319
|
+
* Show all runs in compact format.
|
|
4320
|
+
*/
|
|
4321
|
+
listRuns() {
|
|
4322
|
+
if (runHistory2.length === 0) {
|
|
4323
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4324
|
+
return;
|
|
4325
|
+
}
|
|
4326
|
+
console.table(
|
|
4327
|
+
runHistory2.map((r) => ({
|
|
4328
|
+
id: r.runId.slice(-8),
|
|
4329
|
+
time: r.timestamp.toLocaleTimeString(),
|
|
4330
|
+
mixer: r.mixerType,
|
|
4331
|
+
sources: r.sourceSummaries.length,
|
|
4332
|
+
selected: r.finalCount,
|
|
4333
|
+
reviews: r.reviewsSelected,
|
|
4334
|
+
new: r.newSelected
|
|
4335
|
+
}))
|
|
4336
|
+
);
|
|
4337
|
+
},
|
|
4338
|
+
/**
|
|
4339
|
+
* Export run history as JSON for bug reports.
|
|
4340
|
+
*/
|
|
4341
|
+
export() {
|
|
4342
|
+
const json = JSON.stringify(runHistory2, null, 2);
|
|
4343
|
+
logger.info("[Mixer Debug] Run history exported. Copy the returned string or use:");
|
|
4344
|
+
logger.info(" copy(window.skuilder.mixer.export())");
|
|
4345
|
+
return json;
|
|
4346
|
+
},
|
|
4347
|
+
/**
|
|
4348
|
+
* Clear run history.
|
|
4349
|
+
*/
|
|
4350
|
+
clear() {
|
|
4351
|
+
runHistory2.length = 0;
|
|
4352
|
+
logger.info("[Mixer Debug] Run history cleared.");
|
|
4353
|
+
},
|
|
4354
|
+
/**
|
|
4355
|
+
* Show help.
|
|
4356
|
+
*/
|
|
4357
|
+
help() {
|
|
4358
|
+
logger.info(`
|
|
4359
|
+
\u{1F3A8} Mixer Debug API
|
|
4360
|
+
|
|
4361
|
+
Commands:
|
|
4362
|
+
.showLastMix() Show summary of most recent mixer run
|
|
4363
|
+
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
4364
|
+
.explainSourceBalance() Analyze source balance and selection patterns
|
|
4365
|
+
.compareScores() Compare score distributions across sources
|
|
4366
|
+
.showCard(cardId) Show mixer decisions for a specific card
|
|
4367
|
+
.listRuns() List all captured runs in table format
|
|
4368
|
+
.export() Export run history as JSON for bug reports
|
|
4369
|
+
.clear() Clear run history
|
|
4370
|
+
.runs Access raw run history array
|
|
4371
|
+
.help() Show this help message
|
|
2486
4372
|
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
4373
|
+
Example:
|
|
4374
|
+
window.skuilder.mixer.showLastMix()
|
|
4375
|
+
window.skuilder.mixer.explainSourceBalance()
|
|
4376
|
+
window.skuilder.mixer.compareScores()
|
|
4377
|
+
`);
|
|
4378
|
+
}
|
|
4379
|
+
};
|
|
4380
|
+
mountMixerDebugger();
|
|
2494
4381
|
}
|
|
2495
4382
|
});
|
|
2496
4383
|
|
|
2497
|
-
// src/
|
|
2498
|
-
function
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
hash = Math.imul(hash, 16777619);
|
|
4384
|
+
// src/study/SessionDebugger.ts
|
|
4385
|
+
function showCurrentQueue() {
|
|
4386
|
+
if (!activeSession) {
|
|
4387
|
+
logger.info("[Session Debug] No active session.");
|
|
4388
|
+
return;
|
|
2503
4389
|
}
|
|
2504
|
-
|
|
2505
|
-
}
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
4390
|
+
const latest = activeSession.queueSnapshots[activeSession.queueSnapshots.length - 1] || activeSession.initialQueues;
|
|
4391
|
+
console.group("\u{1F4CA} Current Queue State");
|
|
4392
|
+
logger.info(`Review Queue: ${latest.reviewQLength} cards`);
|
|
4393
|
+
if (latest.reviewQNext3 && latest.reviewQNext3.length > 0) {
|
|
4394
|
+
logger.info(` Next: ${latest.reviewQNext3.join(", ")}`);
|
|
4395
|
+
}
|
|
4396
|
+
logger.info(`New Queue: ${latest.newQLength} cards`);
|
|
4397
|
+
if (latest.newQNext3 && latest.newQNext3.length > 0) {
|
|
4398
|
+
logger.info(` Next: ${latest.newQNext3.join(", ")}`);
|
|
4399
|
+
}
|
|
4400
|
+
logger.info(`Failed Queue: ${latest.failedQLength} cards`);
|
|
4401
|
+
console.groupEnd();
|
|
2515
4402
|
}
|
|
2516
|
-
function
|
|
2517
|
-
const
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
4403
|
+
function showPresentationHistory(sessionIndex = 0) {
|
|
4404
|
+
const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
|
|
4405
|
+
if (!session) {
|
|
4406
|
+
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
4407
|
+
return;
|
|
4408
|
+
}
|
|
4409
|
+
console.group(`\u{1F4DC} Session History: ${session.sessionId}`);
|
|
4410
|
+
logger.info(`Started: ${session.startTime.toLocaleTimeString()}`);
|
|
4411
|
+
if (session.endTime) {
|
|
4412
|
+
logger.info(`Ended: ${session.endTime.toLocaleTimeString()}`);
|
|
4413
|
+
}
|
|
4414
|
+
logger.info(`Cards presented: ${session.presentations.length}`);
|
|
4415
|
+
if (session.presentations.length > 0) {
|
|
4416
|
+
console.table(
|
|
4417
|
+
session.presentations.map((p) => ({
|
|
4418
|
+
"#": p.sequenceNumber,
|
|
4419
|
+
course: p.courseName || p.courseId.slice(0, 8),
|
|
4420
|
+
origin: p.origin,
|
|
4421
|
+
queue: p.queueSource,
|
|
4422
|
+
score: p.score?.toFixed(3) || "-",
|
|
4423
|
+
time: p.timestamp.toLocaleTimeString()
|
|
4424
|
+
}))
|
|
4425
|
+
);
|
|
4426
|
+
}
|
|
4427
|
+
console.groupEnd();
|
|
2522
4428
|
}
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
logger.error(`[Orchestration] Failed to load course config: ${e}`);
|
|
2529
|
-
courseConfig = {
|
|
2530
|
-
name: "Unknown",
|
|
2531
|
-
description: "",
|
|
2532
|
-
public: false,
|
|
2533
|
-
deleted: false,
|
|
2534
|
-
creator: "",
|
|
2535
|
-
admins: [],
|
|
2536
|
-
moderators: [],
|
|
2537
|
-
dataShapes: [],
|
|
2538
|
-
questionTypes: [],
|
|
2539
|
-
orchestration: { salt: "default" }
|
|
2540
|
-
};
|
|
4429
|
+
function showInterleaving(sessionIndex = 0) {
|
|
4430
|
+
const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
|
|
4431
|
+
if (!session) {
|
|
4432
|
+
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
4433
|
+
return;
|
|
2541
4434
|
}
|
|
2542
|
-
|
|
2543
|
-
const
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
2551
|
-
},
|
|
2552
|
-
getDeviation(strategyId) {
|
|
2553
|
-
return computeDeviation(userId, strategyId, salt);
|
|
4435
|
+
console.group("\u{1F500} Interleaving Analysis");
|
|
4436
|
+
const courseCounts = /* @__PURE__ */ new Map();
|
|
4437
|
+
const courseOrigins = /* @__PURE__ */ new Map();
|
|
4438
|
+
session.presentations.forEach((p) => {
|
|
4439
|
+
const name = p.courseName || p.courseId;
|
|
4440
|
+
courseCounts.set(name, (courseCounts.get(name) || 0) + 1);
|
|
4441
|
+
if (!courseOrigins.has(name)) {
|
|
4442
|
+
courseOrigins.set(name, { review: 0, new: 0, failed: 0 });
|
|
2554
4443
|
}
|
|
2555
|
-
|
|
4444
|
+
const origins = courseOrigins.get(name);
|
|
4445
|
+
origins[p.origin]++;
|
|
4446
|
+
});
|
|
4447
|
+
logger.info("Course distribution:");
|
|
4448
|
+
console.table(
|
|
4449
|
+
Array.from(courseCounts.entries()).map(([course, count]) => {
|
|
4450
|
+
const origins = courseOrigins.get(course);
|
|
4451
|
+
return {
|
|
4452
|
+
course,
|
|
4453
|
+
total: count,
|
|
4454
|
+
reviews: origins.review,
|
|
4455
|
+
new: origins.new,
|
|
4456
|
+
failed: origins.failed,
|
|
4457
|
+
percentage: (count / session.presentations.length * 100).toFixed(1) + "%"
|
|
4458
|
+
};
|
|
4459
|
+
})
|
|
4460
|
+
);
|
|
4461
|
+
if (session.presentations.length > 0) {
|
|
4462
|
+
logger.info("\nPresentation sequence (first 20):");
|
|
4463
|
+
const sequence = session.presentations.slice(0, 20).map((p, idx) => `${idx + 1}. ${p.courseName || p.courseId.slice(0, 8)} (${p.origin})`).join("\n");
|
|
4464
|
+
logger.info(sequence);
|
|
4465
|
+
}
|
|
4466
|
+
let maxCluster = 0;
|
|
4467
|
+
let currentCluster = 1;
|
|
4468
|
+
let currentCourse = session.presentations[0]?.courseId;
|
|
4469
|
+
for (let i = 1; i < session.presentations.length; i++) {
|
|
4470
|
+
if (session.presentations[i].courseId === currentCourse) {
|
|
4471
|
+
currentCluster++;
|
|
4472
|
+
maxCluster = Math.max(maxCluster, currentCluster);
|
|
4473
|
+
} else {
|
|
4474
|
+
currentCourse = session.presentations[i].courseId;
|
|
4475
|
+
currentCluster = 1;
|
|
4476
|
+
}
|
|
4477
|
+
}
|
|
4478
|
+
if (maxCluster > 3) {
|
|
4479
|
+
logger.info(`
|
|
4480
|
+
\u26A0\uFE0F Detected clustering: max ${maxCluster} cards from same course in a row`);
|
|
4481
|
+
logger.info("This suggests cards are sorted by score rather than round-robin by course.");
|
|
4482
|
+
}
|
|
4483
|
+
console.groupEnd();
|
|
2556
4484
|
}
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
4485
|
+
function mountSessionDebugger() {
|
|
4486
|
+
if (typeof window === "undefined") return;
|
|
4487
|
+
const win = window;
|
|
4488
|
+
win.skuilder = win.skuilder || {};
|
|
4489
|
+
win.skuilder.session = sessionDebugAPI;
|
|
4490
|
+
}
|
|
4491
|
+
var activeSession, sessionHistory, sessionDebugAPI;
|
|
4492
|
+
var init_SessionDebugger = __esm({
|
|
4493
|
+
"src/study/SessionDebugger.ts"() {
|
|
2560
4494
|
"use strict";
|
|
2561
4495
|
init_logger();
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
4496
|
+
activeSession = null;
|
|
4497
|
+
sessionHistory = [];
|
|
4498
|
+
sessionDebugAPI = {
|
|
4499
|
+
/**
|
|
4500
|
+
* Get raw session history for programmatic access.
|
|
4501
|
+
*/
|
|
4502
|
+
get sessions() {
|
|
4503
|
+
return [...sessionHistory];
|
|
4504
|
+
},
|
|
4505
|
+
/**
|
|
4506
|
+
* Get active session if any.
|
|
4507
|
+
*/
|
|
4508
|
+
get active() {
|
|
4509
|
+
return activeSession;
|
|
4510
|
+
},
|
|
4511
|
+
/**
|
|
4512
|
+
* Show current queue state.
|
|
4513
|
+
*/
|
|
4514
|
+
showQueue() {
|
|
4515
|
+
showCurrentQueue();
|
|
4516
|
+
},
|
|
4517
|
+
/**
|
|
4518
|
+
* Show presentation history for current or past session.
|
|
4519
|
+
*/
|
|
4520
|
+
showHistory(sessionIndex = 0) {
|
|
4521
|
+
showPresentationHistory(sessionIndex);
|
|
4522
|
+
},
|
|
4523
|
+
/**
|
|
4524
|
+
* Analyze course interleaving pattern.
|
|
4525
|
+
*/
|
|
4526
|
+
showInterleaving(sessionIndex = 0) {
|
|
4527
|
+
showInterleaving(sessionIndex);
|
|
4528
|
+
},
|
|
4529
|
+
/**
|
|
4530
|
+
* List all tracked sessions.
|
|
4531
|
+
*/
|
|
4532
|
+
listSessions() {
|
|
4533
|
+
if (activeSession) {
|
|
4534
|
+
logger.info(`Active session: ${activeSession.sessionId} (${activeSession.presentations.length} cards presented)`);
|
|
4535
|
+
}
|
|
4536
|
+
if (sessionHistory.length === 0) {
|
|
4537
|
+
logger.info("[Session Debug] No completed sessions in history.");
|
|
4538
|
+
return;
|
|
4539
|
+
}
|
|
4540
|
+
console.table(
|
|
4541
|
+
sessionHistory.map((s, idx) => ({
|
|
4542
|
+
index: idx,
|
|
4543
|
+
id: s.sessionId.slice(-8),
|
|
4544
|
+
started: s.startTime.toLocaleTimeString(),
|
|
4545
|
+
ended: s.endTime?.toLocaleTimeString() || "incomplete",
|
|
4546
|
+
cards: s.presentations.length
|
|
4547
|
+
}))
|
|
4548
|
+
);
|
|
4549
|
+
},
|
|
4550
|
+
/**
|
|
4551
|
+
* Export session history as JSON for bug reports.
|
|
4552
|
+
*/
|
|
4553
|
+
export() {
|
|
4554
|
+
const data = {
|
|
4555
|
+
active: activeSession,
|
|
4556
|
+
history: sessionHistory
|
|
4557
|
+
};
|
|
4558
|
+
const json = JSON.stringify(data, null, 2);
|
|
4559
|
+
logger.info("[Session Debug] Session data exported. Copy the returned string or use:");
|
|
4560
|
+
logger.info(" copy(window.skuilder.session.export())");
|
|
4561
|
+
return json;
|
|
4562
|
+
},
|
|
4563
|
+
/**
|
|
4564
|
+
* Clear session history.
|
|
4565
|
+
*/
|
|
4566
|
+
clear() {
|
|
4567
|
+
sessionHistory.length = 0;
|
|
4568
|
+
logger.info("[Session Debug] Session history cleared.");
|
|
4569
|
+
},
|
|
4570
|
+
/**
|
|
4571
|
+
* Show help.
|
|
4572
|
+
*/
|
|
4573
|
+
help() {
|
|
4574
|
+
logger.info(`
|
|
4575
|
+
\u{1F3AF} Session Debug API
|
|
4576
|
+
|
|
4577
|
+
Commands:
|
|
4578
|
+
.showQueue() Show current queue state (active session only)
|
|
4579
|
+
.showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
|
|
4580
|
+
.showInterleaving(index?) Analyze course interleaving pattern
|
|
4581
|
+
.listSessions() List all tracked sessions
|
|
4582
|
+
.export() Export session data as JSON for bug reports
|
|
4583
|
+
.clear() Clear session history
|
|
4584
|
+
.sessions Access raw session history array
|
|
4585
|
+
.active Access active session (if any)
|
|
4586
|
+
.help() Show this help message
|
|
4587
|
+
|
|
4588
|
+
Example:
|
|
4589
|
+
window.skuilder.session.showHistory()
|
|
4590
|
+
window.skuilder.session.showInterleaving()
|
|
4591
|
+
window.skuilder.session.showQueue()
|
|
4592
|
+
`);
|
|
4593
|
+
}
|
|
4594
|
+
};
|
|
4595
|
+
mountSessionDebugger();
|
|
4596
|
+
}
|
|
4597
|
+
});
|
|
4598
|
+
|
|
4599
|
+
// src/study/SessionController.ts
|
|
4600
|
+
var init_SessionController = __esm({
|
|
4601
|
+
"src/study/SessionController.ts"() {
|
|
4602
|
+
"use strict";
|
|
4603
|
+
init_SrsService();
|
|
4604
|
+
init_EloService();
|
|
4605
|
+
init_ResponseProcessor();
|
|
4606
|
+
init_CardHydrationService();
|
|
4607
|
+
init_ItemQueue();
|
|
4608
|
+
init_couch();
|
|
2565
4609
|
init_recording();
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
4610
|
+
init_util2();
|
|
4611
|
+
init_navigators();
|
|
4612
|
+
init_SourceMixer();
|
|
4613
|
+
init_MixerDebugger();
|
|
4614
|
+
init_SessionDebugger();
|
|
4615
|
+
init_logger();
|
|
2570
4616
|
}
|
|
2571
4617
|
});
|
|
2572
4618
|
|
|
@@ -2650,15 +4696,16 @@ function logCardProvenance(cards, maxCards = 3) {
|
|
|
2650
4696
|
}
|
|
2651
4697
|
}
|
|
2652
4698
|
}
|
|
2653
|
-
var
|
|
4699
|
+
var import_common12, VERBOSE_RESULTS, Pipeline;
|
|
2654
4700
|
var init_Pipeline = __esm({
|
|
2655
4701
|
"src/core/navigators/Pipeline.ts"() {
|
|
2656
4702
|
"use strict";
|
|
2657
|
-
|
|
4703
|
+
import_common12 = require("@vue-skuilder/common");
|
|
2658
4704
|
init_navigators();
|
|
2659
4705
|
init_logger();
|
|
2660
4706
|
init_orchestration();
|
|
2661
4707
|
init_PipelineDebugger();
|
|
4708
|
+
init_SessionController();
|
|
2662
4709
|
VERBOSE_RESULTS = true;
|
|
2663
4710
|
Pipeline = class extends ContentNavigator {
|
|
2664
4711
|
generator;
|
|
@@ -2818,8 +4865,9 @@ var init_Pipeline = __esm({
|
|
|
2818
4865
|
generatorSummaries,
|
|
2819
4866
|
generatedCount,
|
|
2820
4867
|
filterImpacts,
|
|
2821
|
-
|
|
2822
|
-
result
|
|
4868
|
+
cards,
|
|
4869
|
+
result,
|
|
4870
|
+
context.userElo
|
|
2823
4871
|
);
|
|
2824
4872
|
captureRun(report);
|
|
2825
4873
|
} catch (e) {
|
|
@@ -2902,7 +4950,7 @@ var init_Pipeline = __esm({
|
|
|
2902
4950
|
card.provenance.push({
|
|
2903
4951
|
strategy: "ephemeralHint",
|
|
2904
4952
|
strategyId: "ephemeral-hint",
|
|
2905
|
-
strategyName: "Replan Hint",
|
|
4953
|
+
strategyName: hints._label ? `Replan Hint (${hints._label})` : "Replan Hint",
|
|
2906
4954
|
action: "boosted",
|
|
2907
4955
|
score: card.score,
|
|
2908
4956
|
reason: `boostTag ${pattern} \xD7${factor}`
|
|
@@ -2919,7 +4967,7 @@ var init_Pipeline = __esm({
|
|
|
2919
4967
|
card.provenance.push({
|
|
2920
4968
|
strategy: "ephemeralHint",
|
|
2921
4969
|
strategyId: "ephemeral-hint",
|
|
2922
|
-
strategyName: "Replan Hint",
|
|
4970
|
+
strategyName: hints._label ? `Replan Hint (${hints._label})` : "Replan Hint",
|
|
2923
4971
|
action: "boosted",
|
|
2924
4972
|
score: card.score,
|
|
2925
4973
|
reason: `boostCard ${pattern} \xD7${factor}`
|
|
@@ -2929,6 +4977,7 @@ var init_Pipeline = __esm({
|
|
|
2929
4977
|
}
|
|
2930
4978
|
}
|
|
2931
4979
|
const cardIds = new Set(cards.map((c) => c.cardId));
|
|
4980
|
+
const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
|
|
2932
4981
|
const inject = (card, reason) => {
|
|
2933
4982
|
if (!cardIds.has(card.cardId)) {
|
|
2934
4983
|
const floorScore = Math.max(card.score, 1);
|
|
@@ -2940,7 +4989,7 @@ var init_Pipeline = __esm({
|
|
|
2940
4989
|
{
|
|
2941
4990
|
strategy: "ephemeralHint",
|
|
2942
4991
|
strategyId: "ephemeral-hint",
|
|
2943
|
-
strategyName:
|
|
4992
|
+
strategyName: hintLabel,
|
|
2944
4993
|
action: "boosted",
|
|
2945
4994
|
score: floorScore,
|
|
2946
4995
|
reason
|
|
@@ -2979,7 +5028,7 @@ var init_Pipeline = __esm({
|
|
|
2979
5028
|
let userElo = 1e3;
|
|
2980
5029
|
try {
|
|
2981
5030
|
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
2982
|
-
const courseElo = (0,
|
|
5031
|
+
const courseElo = (0, import_common12.toCourseElo)(courseReg.elo);
|
|
2983
5032
|
userElo = courseElo.global.score;
|
|
2984
5033
|
} catch (e) {
|
|
2985
5034
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
@@ -3032,6 +5081,34 @@ var init_Pipeline = __esm({
|
|
|
3032
5081
|
return [...new Set(ids)];
|
|
3033
5082
|
}
|
|
3034
5083
|
// ---------------------------------------------------------------------------
|
|
5084
|
+
// Tag ELO diagnostic
|
|
5085
|
+
// ---------------------------------------------------------------------------
|
|
5086
|
+
/**
|
|
5087
|
+
* Get the user's per-tag ELO data for specified tags (or all tags).
|
|
5088
|
+
* Useful for diagnosing why hierarchy gates are open/closed.
|
|
5089
|
+
*/
|
|
5090
|
+
async getTagEloStatus(tagFilter) {
|
|
5091
|
+
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
5092
|
+
const courseElo = (0, import_common12.toCourseElo)(courseReg.elo);
|
|
5093
|
+
const result = {};
|
|
5094
|
+
if (!tagFilter) {
|
|
5095
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
5096
|
+
result[tag] = { score: data.score, count: data.count };
|
|
5097
|
+
}
|
|
5098
|
+
} else {
|
|
5099
|
+
const patterns = Array.isArray(tagFilter) ? tagFilter : [tagFilter];
|
|
5100
|
+
for (const pattern of patterns) {
|
|
5101
|
+
const regex = globToRegex(pattern);
|
|
5102
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
5103
|
+
if (regex.test(tag)) {
|
|
5104
|
+
result[tag] = { score: data.score, count: data.count };
|
|
5105
|
+
}
|
|
5106
|
+
}
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
return result;
|
|
5110
|
+
}
|
|
5111
|
+
// ---------------------------------------------------------------------------
|
|
3035
5112
|
// Card-space diagnostic
|
|
3036
5113
|
// ---------------------------------------------------------------------------
|
|
3037
5114
|
/**
|
|
@@ -3727,11 +5804,11 @@ ${JSON.stringify(config)}
|
|
|
3727
5804
|
function isSuccessRow(row) {
|
|
3728
5805
|
return "doc" in row && row.doc !== null && row.doc !== void 0;
|
|
3729
5806
|
}
|
|
3730
|
-
var
|
|
5807
|
+
var import_common13, CoursesDB, CourseDB;
|
|
3731
5808
|
var init_courseDB = __esm({
|
|
3732
5809
|
"src/impl/couch/courseDB.ts"() {
|
|
3733
5810
|
"use strict";
|
|
3734
|
-
|
|
5811
|
+
import_common13 = require("@vue-skuilder/common");
|
|
3735
5812
|
init_couch();
|
|
3736
5813
|
init_updateQueue();
|
|
3737
5814
|
init_types_legacy();
|
|
@@ -3880,14 +5957,14 @@ var init_courseDB = __esm({
|
|
|
3880
5957
|
docs.rows.forEach((r) => {
|
|
3881
5958
|
if (isSuccessRow(r)) {
|
|
3882
5959
|
if (r.doc && r.doc.elo) {
|
|
3883
|
-
ret.push((0,
|
|
5960
|
+
ret.push((0, import_common13.toCourseElo)(r.doc.elo));
|
|
3884
5961
|
} else {
|
|
3885
5962
|
logger.warn("no elo data for card: " + r.id);
|
|
3886
|
-
ret.push((0,
|
|
5963
|
+
ret.push((0, import_common13.blankCourseElo)());
|
|
3887
5964
|
}
|
|
3888
5965
|
} else {
|
|
3889
5966
|
logger.warn("no elo data for card: " + JSON.stringify(r));
|
|
3890
|
-
ret.push((0,
|
|
5967
|
+
ret.push((0, import_common13.blankCourseElo)());
|
|
3891
5968
|
}
|
|
3892
5969
|
});
|
|
3893
5970
|
return ret;
|
|
@@ -4089,7 +6166,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
4089
6166
|
async getCourseTagStubs() {
|
|
4090
6167
|
return getCourseTagStubs(this.id);
|
|
4091
6168
|
}
|
|
4092
|
-
async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0,
|
|
6169
|
+
async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common13.blankCourseElo)()) {
|
|
4093
6170
|
try {
|
|
4094
6171
|
const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
|
|
4095
6172
|
if (resp.ok) {
|
|
@@ -4098,19 +6175,19 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
4098
6175
|
`[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
|
|
4099
6176
|
);
|
|
4100
6177
|
return {
|
|
4101
|
-
status:
|
|
6178
|
+
status: import_common13.Status.error,
|
|
4102
6179
|
message: `Note was added but no cards were created: ${resp.cardCreationError}`,
|
|
4103
6180
|
id: resp.id
|
|
4104
6181
|
};
|
|
4105
6182
|
}
|
|
4106
6183
|
return {
|
|
4107
|
-
status:
|
|
6184
|
+
status: import_common13.Status.ok,
|
|
4108
6185
|
message: "",
|
|
4109
6186
|
id: resp.id
|
|
4110
6187
|
};
|
|
4111
6188
|
} else {
|
|
4112
6189
|
return {
|
|
4113
|
-
status:
|
|
6190
|
+
status: import_common13.Status.error,
|
|
4114
6191
|
message: "Unexpected error adding note"
|
|
4115
6192
|
};
|
|
4116
6193
|
}
|
|
@@ -4122,7 +6199,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
4122
6199
|
message: ${err.message}`
|
|
4123
6200
|
);
|
|
4124
6201
|
return {
|
|
4125
|
-
status:
|
|
6202
|
+
status: import_common13.Status.error,
|
|
4126
6203
|
message: `Error adding note to course. ${e.reason || err.message}`
|
|
4127
6204
|
};
|
|
4128
6205
|
}
|
|
@@ -4231,10 +6308,18 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
4231
6308
|
* @param limit - Maximum number of cards to return
|
|
4232
6309
|
* @returns Cards sorted by score descending
|
|
4233
6310
|
*/
|
|
6311
|
+
_pendingHints = null;
|
|
6312
|
+
setEphemeralHints(hints) {
|
|
6313
|
+
this._pendingHints = hints;
|
|
6314
|
+
}
|
|
4234
6315
|
async getWeightedCards(limit) {
|
|
4235
6316
|
const u = await this._getCurrentUser();
|
|
4236
6317
|
try {
|
|
4237
6318
|
const navigator = await this.createNavigator(u);
|
|
6319
|
+
if (this._pendingHints) {
|
|
6320
|
+
navigator.setEphemeralHints(this._pendingHints);
|
|
6321
|
+
this._pendingHints = null;
|
|
6322
|
+
}
|
|
4238
6323
|
return navigator.getWeightedCards(limit);
|
|
4239
6324
|
} catch (e) {
|
|
4240
6325
|
logger.error(`[courseDB] Error getting weighted cards: ${e}`);
|
|
@@ -4253,7 +6338,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
4253
6338
|
const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
|
|
4254
6339
|
return c.courseID === this.id;
|
|
4255
6340
|
});
|
|
4256
|
-
targetElo = (0,
|
|
6341
|
+
targetElo = (0, import_common13.EloToNumber)(courseDoc.elo);
|
|
4257
6342
|
} catch {
|
|
4258
6343
|
targetElo = 1e3;
|
|
4259
6344
|
}
|
|
@@ -4388,13 +6473,13 @@ function getClassroomDB(classID, version) {
|
|
|
4388
6473
|
async function getClassroomConfig(classID) {
|
|
4389
6474
|
return await getClassroomDB(classID, "student").get(CLASSROOM_CONFIG);
|
|
4390
6475
|
}
|
|
4391
|
-
var
|
|
6476
|
+
var import_moment4, classroomLookupDBTitle, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB, TeacherClassroomDB, ClassroomLookupDB;
|
|
4392
6477
|
var init_classroomDB2 = __esm({
|
|
4393
6478
|
"src/impl/couch/classroomDB.ts"() {
|
|
4394
6479
|
"use strict";
|
|
4395
6480
|
init_factory();
|
|
4396
6481
|
init_logger();
|
|
4397
|
-
|
|
6482
|
+
import_moment4 = __toESM(require("moment"), 1);
|
|
4398
6483
|
init_pouchdb_setup();
|
|
4399
6484
|
init_couch();
|
|
4400
6485
|
init_courseDB();
|
|
@@ -4507,9 +6592,9 @@ var init_classroomDB2 = __esm({
|
|
|
4507
6592
|
}
|
|
4508
6593
|
const activeCards = await this._user.getActiveCards();
|
|
4509
6594
|
const activeCardIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
4510
|
-
const now =
|
|
6595
|
+
const now = import_moment4.default.utc();
|
|
4511
6596
|
const assigned = await this.getAssignedContent();
|
|
4512
|
-
const due = assigned.filter((c) => now.isAfter(
|
|
6597
|
+
const due = assigned.filter((c) => now.isAfter(import_moment4.default.utc(c.activeOn, REVIEW_TIME_FORMAT)));
|
|
4513
6598
|
logger.info(`[StudentClassroomDB] Due content: ${JSON.stringify(due)}`);
|
|
4514
6599
|
for (const content of due) {
|
|
4515
6600
|
if (content.type === "course") {
|
|
@@ -4635,8 +6720,8 @@ var init_classroomDB2 = __esm({
|
|
|
4635
6720
|
type: "tag",
|
|
4636
6721
|
_id: id,
|
|
4637
6722
|
assignedBy: content.assignedBy,
|
|
4638
|
-
assignedOn:
|
|
4639
|
-
activeOn: content.activeOn ||
|
|
6723
|
+
assignedOn: import_moment4.default.utc(),
|
|
6724
|
+
activeOn: content.activeOn || import_moment4.default.utc()
|
|
4640
6725
|
});
|
|
4641
6726
|
} else {
|
|
4642
6727
|
put = await this._db.put({
|
|
@@ -4644,8 +6729,8 @@ var init_classroomDB2 = __esm({
|
|
|
4644
6729
|
type: "course",
|
|
4645
6730
|
_id: id,
|
|
4646
6731
|
assignedBy: content.assignedBy,
|
|
4647
|
-
assignedOn:
|
|
4648
|
-
activeOn: content.activeOn ||
|
|
6732
|
+
assignedOn: import_moment4.default.utc(),
|
|
6733
|
+
activeOn: content.activeOn || import_moment4.default.utc()
|
|
4649
6734
|
});
|
|
4650
6735
|
}
|
|
4651
6736
|
if (put.ok) {
|
|
@@ -4665,11 +6750,11 @@ var init_classroomDB2 = __esm({
|
|
|
4665
6750
|
});
|
|
4666
6751
|
|
|
4667
6752
|
// src/study/TagFilteredContentSource.ts
|
|
4668
|
-
var
|
|
6753
|
+
var import_common14, TagFilteredContentSource;
|
|
4669
6754
|
var init_TagFilteredContentSource = __esm({
|
|
4670
6755
|
"src/study/TagFilteredContentSource.ts"() {
|
|
4671
6756
|
"use strict";
|
|
4672
|
-
|
|
6757
|
+
import_common14 = require("@vue-skuilder/common");
|
|
4673
6758
|
init_courseDB();
|
|
4674
6759
|
init_logger();
|
|
4675
6760
|
TagFilteredContentSource = class {
|
|
@@ -4755,7 +6840,7 @@ var init_TagFilteredContentSource = __esm({
|
|
|
4755
6840
|
* @returns Cards sorted by score descending (all scores = 1.0)
|
|
4756
6841
|
*/
|
|
4757
6842
|
async getWeightedCards(limit) {
|
|
4758
|
-
if (!(0,
|
|
6843
|
+
if (!(0, import_common14.hasActiveFilter)(this.filter)) {
|
|
4759
6844
|
logger.warn("[TagFilteredContentSource] getWeightedCards called with no active filter");
|
|
4760
6845
|
return [];
|
|
4761
6846
|
}
|
|
@@ -4843,19 +6928,19 @@ async function getStudySource(source, user) {
|
|
|
4843
6928
|
if (source.type === "classroom") {
|
|
4844
6929
|
return await StudentClassroomDB.factory(source.id, user);
|
|
4845
6930
|
} else {
|
|
4846
|
-
if ((0,
|
|
6931
|
+
if ((0, import_common15.hasActiveFilter)(source.tagFilter)) {
|
|
4847
6932
|
return new TagFilteredContentSource(source.id, source.tagFilter, user);
|
|
4848
6933
|
}
|
|
4849
6934
|
return getDataLayer().getCourseDB(source.id);
|
|
4850
6935
|
}
|
|
4851
6936
|
}
|
|
4852
|
-
var
|
|
6937
|
+
var import_common15;
|
|
4853
6938
|
var init_contentSource = __esm({
|
|
4854
6939
|
"src/core/interfaces/contentSource.ts"() {
|
|
4855
6940
|
"use strict";
|
|
4856
6941
|
init_factory();
|
|
4857
6942
|
init_classroomDB2();
|
|
4858
|
-
|
|
6943
|
+
import_common15 = require("@vue-skuilder/common");
|
|
4859
6944
|
init_TagFilteredContentSource();
|
|
4860
6945
|
}
|
|
4861
6946
|
});
|
|
@@ -4918,29 +7003,18 @@ var init_userOutcome = __esm({
|
|
|
4918
7003
|
}
|
|
4919
7004
|
});
|
|
4920
7005
|
|
|
4921
|
-
// src/core/util/index.ts
|
|
4922
|
-
function getCardHistoryID(courseID, cardID) {
|
|
4923
|
-
return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
|
|
4924
|
-
}
|
|
4925
|
-
var init_util = __esm({
|
|
4926
|
-
"src/core/util/index.ts"() {
|
|
4927
|
-
"use strict";
|
|
4928
|
-
init_types_legacy();
|
|
4929
|
-
}
|
|
4930
|
-
});
|
|
4931
|
-
|
|
4932
7006
|
// src/core/bulkImport/cardProcessor.ts
|
|
4933
|
-
var
|
|
7007
|
+
var import_common16;
|
|
4934
7008
|
var init_cardProcessor = __esm({
|
|
4935
7009
|
"src/core/bulkImport/cardProcessor.ts"() {
|
|
4936
7010
|
"use strict";
|
|
4937
|
-
|
|
7011
|
+
import_common16 = require("@vue-skuilder/common");
|
|
4938
7012
|
init_logger();
|
|
4939
7013
|
}
|
|
4940
7014
|
});
|
|
4941
7015
|
|
|
4942
7016
|
// src/core/bulkImport/types.ts
|
|
4943
|
-
var
|
|
7017
|
+
var init_types5 = __esm({
|
|
4944
7018
|
"src/core/bulkImport/types.ts"() {
|
|
4945
7019
|
"use strict";
|
|
4946
7020
|
}
|
|
@@ -4951,29 +7025,7 @@ var init_bulkImport = __esm({
|
|
|
4951
7025
|
"src/core/bulkImport/index.ts"() {
|
|
4952
7026
|
"use strict";
|
|
4953
7027
|
init_cardProcessor();
|
|
4954
|
-
|
|
4955
|
-
}
|
|
4956
|
-
});
|
|
4957
|
-
|
|
4958
|
-
// src/util/dataDirectory.ts
|
|
4959
|
-
function getAppDataDirectory() {
|
|
4960
|
-
if (ENV.LOCAL_STORAGE_PREFIX) {
|
|
4961
|
-
return path.join(os.homedir(), `.tuilder`, ENV.LOCAL_STORAGE_PREFIX);
|
|
4962
|
-
} else {
|
|
4963
|
-
return path.join(os.homedir(), ".tuilder");
|
|
4964
|
-
}
|
|
4965
|
-
}
|
|
4966
|
-
function getDbPath(dbName) {
|
|
4967
|
-
return path.join(getAppDataDirectory(), dbName);
|
|
4968
|
-
}
|
|
4969
|
-
var path, os;
|
|
4970
|
-
var init_dataDirectory = __esm({
|
|
4971
|
-
"src/util/dataDirectory.ts"() {
|
|
4972
|
-
"use strict";
|
|
4973
|
-
path = __toESM(require("path"), 1);
|
|
4974
|
-
os = __toESM(require("os"), 1);
|
|
4975
|
-
init_logger();
|
|
4976
|
-
init_factory();
|
|
7028
|
+
init_types5();
|
|
4977
7029
|
}
|
|
4978
7030
|
});
|
|
4979
7031
|
|
|
@@ -5005,7 +7057,7 @@ function getStartAndEndKeys2(key) {
|
|
|
5005
7057
|
};
|
|
5006
7058
|
}
|
|
5007
7059
|
function updateGuestAccountExpirationDate(guestDB) {
|
|
5008
|
-
const currentTime =
|
|
7060
|
+
const currentTime = import_moment5.default.utc();
|
|
5009
7061
|
const expirationDate = currentTime.add(2, "months").toISOString();
|
|
5010
7062
|
const expiryDocID2 = "GuestAccountExpirationDate";
|
|
5011
7063
|
void guestDB.get(expiryDocID2).then((doc) => {
|
|
@@ -5030,7 +7082,7 @@ function getLocalUserDB(username) {
|
|
|
5030
7082
|
}
|
|
5031
7083
|
}
|
|
5032
7084
|
function scheduleCardReviewLocal(userDB, review) {
|
|
5033
|
-
const now =
|
|
7085
|
+
const now = import_moment5.default.utc();
|
|
5034
7086
|
logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
|
|
5035
7087
|
void userDB.put({
|
|
5036
7088
|
_id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT2),
|
|
@@ -5053,11 +7105,11 @@ async function removeScheduledCardReviewLocal(userDB, reviewDocID) {
|
|
|
5053
7105
|
${JSON.stringify(err)}`);
|
|
5054
7106
|
});
|
|
5055
7107
|
}
|
|
5056
|
-
var
|
|
7108
|
+
var import_moment5, REVIEW_TIME_FORMAT2, log2;
|
|
5057
7109
|
var init_userDBHelpers = __esm({
|
|
5058
7110
|
"src/impl/common/userDBHelpers.ts"() {
|
|
5059
7111
|
"use strict";
|
|
5060
|
-
|
|
7112
|
+
import_moment5 = __toESM(require("moment"), 1);
|
|
5061
7113
|
init_core();
|
|
5062
7114
|
init_logger();
|
|
5063
7115
|
init_pouchdb_setup();
|
|
@@ -5417,11 +7469,11 @@ var init_core = __esm({
|
|
|
5417
7469
|
});
|
|
5418
7470
|
|
|
5419
7471
|
// src/impl/couch/user-course-relDB.ts
|
|
5420
|
-
var
|
|
7472
|
+
var import_moment6, UsrCrsData;
|
|
5421
7473
|
var init_user_course_relDB = __esm({
|
|
5422
7474
|
"src/impl/couch/user-course-relDB.ts"() {
|
|
5423
7475
|
"use strict";
|
|
5424
|
-
|
|
7476
|
+
import_moment6 = __toESM(require("moment"), 1);
|
|
5425
7477
|
init_logger();
|
|
5426
7478
|
UsrCrsData = class {
|
|
5427
7479
|
user;
|
|
@@ -5431,11 +7483,11 @@ var init_user_course_relDB = __esm({
|
|
|
5431
7483
|
this._courseId = courseId;
|
|
5432
7484
|
}
|
|
5433
7485
|
async getReviewsForcast(daysCount) {
|
|
5434
|
-
const time =
|
|
7486
|
+
const time = import_moment6.default.utc().add(daysCount, "days");
|
|
5435
7487
|
return this.getReviewstoDate(time);
|
|
5436
7488
|
}
|
|
5437
7489
|
async getPendingReviews() {
|
|
5438
|
-
const now =
|
|
7490
|
+
const now = import_moment6.default.utc();
|
|
5439
7491
|
return this.getReviewstoDate(now);
|
|
5440
7492
|
}
|
|
5441
7493
|
async getScheduledReviewCount() {
|
|
@@ -5471,7 +7523,7 @@ var init_user_course_relDB = __esm({
|
|
|
5471
7523
|
`Fetching ${this.user.getUsername()}'s scheduled reviews for course ${this._courseId}.`
|
|
5472
7524
|
);
|
|
5473
7525
|
return allReviews.filter((review) => {
|
|
5474
|
-
const reviewTime =
|
|
7526
|
+
const reviewTime = import_moment6.default.utc(review.reviewTime);
|
|
5475
7527
|
return targetDate.isAfter(reviewTime);
|
|
5476
7528
|
});
|
|
5477
7529
|
}
|
|
@@ -5653,14 +7705,14 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
5653
7705
|
async function getUserClassrooms(user) {
|
|
5654
7706
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
5655
7707
|
}
|
|
5656
|
-
var
|
|
7708
|
+
var import_common17, import_moment7, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
5657
7709
|
var init_BaseUserDB = __esm({
|
|
5658
7710
|
"src/impl/common/BaseUserDB.ts"() {
|
|
5659
7711
|
"use strict";
|
|
5660
7712
|
init_core();
|
|
5661
7713
|
init_util();
|
|
5662
|
-
|
|
5663
|
-
|
|
7714
|
+
import_common17 = require("@vue-skuilder/common");
|
|
7715
|
+
import_moment7 = __toESM(require("moment"), 1);
|
|
5664
7716
|
init_types_legacy();
|
|
5665
7717
|
init_logger();
|
|
5666
7718
|
init_userDBHelpers();
|
|
@@ -5709,7 +7761,7 @@ Currently logged-in as ${this._username}.`
|
|
|
5709
7761
|
);
|
|
5710
7762
|
}
|
|
5711
7763
|
const result = await this.syncStrategy.createAccount(username, password);
|
|
5712
|
-
if (result.status ===
|
|
7764
|
+
if (result.status === import_common17.Status.ok) {
|
|
5713
7765
|
log3(`Account created successfully, updating username to ${username}`);
|
|
5714
7766
|
this._username = username;
|
|
5715
7767
|
try {
|
|
@@ -5751,7 +7803,7 @@ Currently logged-in as ${this._username}.`
|
|
|
5751
7803
|
async resetUserData() {
|
|
5752
7804
|
if (this.syncStrategy.canAuthenticate()) {
|
|
5753
7805
|
return {
|
|
5754
|
-
status:
|
|
7806
|
+
status: import_common17.Status.error,
|
|
5755
7807
|
error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
|
|
5756
7808
|
};
|
|
5757
7809
|
}
|
|
@@ -5773,11 +7825,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5773
7825
|
await localDB.bulkDocs(docsToDelete);
|
|
5774
7826
|
}
|
|
5775
7827
|
await this.init();
|
|
5776
|
-
return { status:
|
|
7828
|
+
return { status: import_common17.Status.ok };
|
|
5777
7829
|
} catch (error) {
|
|
5778
7830
|
logger.error("Failed to reset user data:", error);
|
|
5779
7831
|
return {
|
|
5780
|
-
status:
|
|
7832
|
+
status: import_common17.Status.error,
|
|
5781
7833
|
error: error instanceof Error ? error.message : "Unknown error during reset"
|
|
5782
7834
|
};
|
|
5783
7835
|
}
|
|
@@ -5924,7 +7976,7 @@ Currently logged-in as ${this._username}.`
|
|
|
5924
7976
|
);
|
|
5925
7977
|
return reviews.rows.filter((r) => {
|
|
5926
7978
|
if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
|
|
5927
|
-
const date =
|
|
7979
|
+
const date = import_moment7.default.utc(
|
|
5928
7980
|
r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
|
|
5929
7981
|
REVIEW_TIME_FORMAT2
|
|
5930
7982
|
);
|
|
@@ -5937,11 +7989,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5937
7989
|
}).map((r) => r.doc);
|
|
5938
7990
|
}
|
|
5939
7991
|
async getReviewsForcast(daysCount) {
|
|
5940
|
-
const time =
|
|
7992
|
+
const time = import_moment7.default.utc().add(daysCount, "days");
|
|
5941
7993
|
return this.getReviewstoDate(time);
|
|
5942
7994
|
}
|
|
5943
7995
|
async getPendingReviews(course_id) {
|
|
5944
|
-
const now =
|
|
7996
|
+
const now = import_moment7.default.utc();
|
|
5945
7997
|
return this.getReviewstoDate(now, course_id);
|
|
5946
7998
|
}
|
|
5947
7999
|
async getScheduledReviewCount(course_id) {
|
|
@@ -6228,7 +8280,7 @@ Currently logged-in as ${this._username}.`
|
|
|
6228
8280
|
*/
|
|
6229
8281
|
async putCardRecord(record) {
|
|
6230
8282
|
const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
|
|
6231
|
-
record.timeStamp =
|
|
8283
|
+
record.timeStamp = import_moment7.default.utc(record.timeStamp).toString();
|
|
6232
8284
|
try {
|
|
6233
8285
|
const cardHistory = await this.update(
|
|
6234
8286
|
cardHistoryID,
|
|
@@ -6244,7 +8296,7 @@ Currently logged-in as ${this._username}.`
|
|
|
6244
8296
|
const ret = {
|
|
6245
8297
|
...record2
|
|
6246
8298
|
};
|
|
6247
|
-
ret.timeStamp =
|
|
8299
|
+
ret.timeStamp = import_moment7.default.utc(record2.timeStamp);
|
|
6248
8300
|
return ret;
|
|
6249
8301
|
});
|
|
6250
8302
|
return cardHistory;
|
|
@@ -6699,8 +8751,20 @@ var init_CourseSyncService = __esm({
|
|
|
6699
8751
|
*/
|
|
6700
8752
|
async ensureSynced(courseId, forceEnabled) {
|
|
6701
8753
|
const existing = this.entries.get(courseId);
|
|
6702
|
-
if (existing?.status.state === "ready") {
|
|
6703
|
-
|
|
8754
|
+
if (existing?.status.state === "ready" && existing.localDB) {
|
|
8755
|
+
const stale = await this.isLocalEpochStale(courseId, existing.localDB);
|
|
8756
|
+
if (!stale) {
|
|
8757
|
+
return;
|
|
8758
|
+
}
|
|
8759
|
+
logger.info(
|
|
8760
|
+
`[CourseSyncService] Remote DB epoch changed for course ${courseId} \u2014 destroying stale local replica`
|
|
8761
|
+
);
|
|
8762
|
+
try {
|
|
8763
|
+
await existing.localDB.destroy();
|
|
8764
|
+
} catch {
|
|
8765
|
+
}
|
|
8766
|
+
existing.localDB = null;
|
|
8767
|
+
existing.readyPromise = null;
|
|
6704
8768
|
}
|
|
6705
8769
|
if (existing?.status.state === "disabled") {
|
|
6706
8770
|
return;
|
|
@@ -6764,7 +8828,15 @@ var init_CourseSyncService = __esm({
|
|
|
6764
8828
|
}
|
|
6765
8829
|
entry.status = { state: "syncing" };
|
|
6766
8830
|
const localDBName = this.localDBName(courseId);
|
|
6767
|
-
|
|
8831
|
+
let localDB = new pouchdb_setup_default(localDBName);
|
|
8832
|
+
const stale = await this.isLocalEpochStale(courseId, localDB);
|
|
8833
|
+
if (stale) {
|
|
8834
|
+
logger.info(
|
|
8835
|
+
`[CourseSyncService] Stale local DB detected for course ${courseId} \u2014 destroying before sync`
|
|
8836
|
+
);
|
|
8837
|
+
await localDB.destroy();
|
|
8838
|
+
localDB = new pouchdb_setup_default(localDBName);
|
|
8839
|
+
}
|
|
6768
8840
|
entry.localDB = localDB;
|
|
6769
8841
|
const remoteDB = this.getRemoteDB(courseId);
|
|
6770
8842
|
const syncStart = Date.now();
|
|
@@ -6854,6 +8926,33 @@ var init_CourseSyncService = __esm({
|
|
|
6854
8926
|
}
|
|
6855
8927
|
}
|
|
6856
8928
|
}
|
|
8929
|
+
/**
|
|
8930
|
+
* Check whether the local replica's `db-epoch` doc matches the remote.
|
|
8931
|
+
*
|
|
8932
|
+
* The seed script (and optionally upload-cards) writes a `db-epoch`
|
|
8933
|
+
* document with a numeric timestamp. If the remote epoch differs from
|
|
8934
|
+
* the local copy, the remote DB was recreated (e.g., `yarn db:seed`)
|
|
8935
|
+
* and the local PouchDB is stale.
|
|
8936
|
+
*
|
|
8937
|
+
* Returns `true` if stale (epoch mismatch or remote has epoch but local
|
|
8938
|
+
* doesn't). Returns `false` (not stale) if epochs match, or if the
|
|
8939
|
+
* remote doesn't have an epoch doc at all (backwards compat).
|
|
8940
|
+
*/
|
|
8941
|
+
async isLocalEpochStale(courseId, localDB) {
|
|
8942
|
+
try {
|
|
8943
|
+
const remoteDB = this.getRemoteDB(courseId);
|
|
8944
|
+
const remoteEpoch = await remoteDB.get("db-epoch");
|
|
8945
|
+
let localEpoch = null;
|
|
8946
|
+
try {
|
|
8947
|
+
localEpoch = await localDB.get("db-epoch");
|
|
8948
|
+
} catch {
|
|
8949
|
+
return true;
|
|
8950
|
+
}
|
|
8951
|
+
return remoteEpoch.epoch !== localEpoch.epoch;
|
|
8952
|
+
} catch {
|
|
8953
|
+
return false;
|
|
8954
|
+
}
|
|
8955
|
+
}
|
|
6857
8956
|
/**
|
|
6858
8957
|
* Get a remote PouchDB handle for a course.
|
|
6859
8958
|
*/
|
|
@@ -6913,14 +9012,14 @@ var init_auth = __esm({
|
|
|
6913
9012
|
});
|
|
6914
9013
|
|
|
6915
9014
|
// src/impl/couch/CouchDBSyncStrategy.ts
|
|
6916
|
-
var
|
|
9015
|
+
var import_common19, log4, CouchDBSyncStrategy;
|
|
6917
9016
|
var init_CouchDBSyncStrategy = __esm({
|
|
6918
9017
|
"src/impl/couch/CouchDBSyncStrategy.ts"() {
|
|
6919
9018
|
"use strict";
|
|
6920
9019
|
init_factory();
|
|
6921
9020
|
init_types_legacy();
|
|
6922
9021
|
init_logger();
|
|
6923
|
-
|
|
9022
|
+
import_common19 = require("@vue-skuilder/common");
|
|
6924
9023
|
init_common();
|
|
6925
9024
|
init_pouchdb_setup();
|
|
6926
9025
|
init_couch();
|
|
@@ -6991,32 +9090,32 @@ var init_CouchDBSyncStrategy = __esm({
|
|
|
6991
9090
|
}
|
|
6992
9091
|
}
|
|
6993
9092
|
return {
|
|
6994
|
-
status:
|
|
9093
|
+
status: import_common19.Status.ok,
|
|
6995
9094
|
error: void 0
|
|
6996
9095
|
};
|
|
6997
9096
|
} else {
|
|
6998
9097
|
return {
|
|
6999
|
-
status:
|
|
9098
|
+
status: import_common19.Status.error,
|
|
7000
9099
|
error: "Failed to log in after account creation"
|
|
7001
9100
|
};
|
|
7002
9101
|
}
|
|
7003
9102
|
} else {
|
|
7004
9103
|
logger.warn(`Signup not OK: ${JSON.stringify(signupRequest)}`);
|
|
7005
9104
|
return {
|
|
7006
|
-
status:
|
|
9105
|
+
status: import_common19.Status.error,
|
|
7007
9106
|
error: "Account creation failed"
|
|
7008
9107
|
};
|
|
7009
9108
|
}
|
|
7010
9109
|
} catch (e) {
|
|
7011
9110
|
if (e.reason === "Document update conflict.") {
|
|
7012
9111
|
return {
|
|
7013
|
-
status:
|
|
9112
|
+
status: import_common19.Status.error,
|
|
7014
9113
|
error: "This username is taken!"
|
|
7015
9114
|
};
|
|
7016
9115
|
}
|
|
7017
9116
|
logger.error(`Error on signup: ${JSON.stringify(e)}`);
|
|
7018
9117
|
return {
|
|
7019
|
-
status:
|
|
9118
|
+
status: import_common19.Status.error,
|
|
7020
9119
|
error: e.message || "Unknown error during account creation"
|
|
7021
9120
|
};
|
|
7022
9121
|
}
|
|
@@ -7256,7 +9355,7 @@ async function usernameIsAvailable(username) {
|
|
|
7256
9355
|
}
|
|
7257
9356
|
}
|
|
7258
9357
|
function updateGuestAccountExpirationDate2(guestDB) {
|
|
7259
|
-
const currentTime =
|
|
9358
|
+
const currentTime = import_moment8.default.utc();
|
|
7260
9359
|
const expirationDate = currentTime.add(2, "months").toISOString();
|
|
7261
9360
|
void guestDB.get(expiryDocID).then((doc) => {
|
|
7262
9361
|
return guestDB.put({
|
|
@@ -7319,7 +9418,7 @@ function getCouchUserDB(username) {
|
|
|
7319
9418
|
return ret;
|
|
7320
9419
|
}
|
|
7321
9420
|
function scheduleCardReview(review) {
|
|
7322
|
-
const now =
|
|
9421
|
+
const now = import_moment8.default.utc();
|
|
7323
9422
|
logger.info(`Scheduling for review in: ${review.time.diff(now, "h") / 24} days`);
|
|
7324
9423
|
void getCouchUserDB(review.user).put({
|
|
7325
9424
|
_id: DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */] + review.time.format(REVIEW_TIME_FORMAT),
|
|
@@ -7348,13 +9447,13 @@ function getStartAndEndKeys(key) {
|
|
|
7348
9447
|
endkey: key + "\uFFF0"
|
|
7349
9448
|
};
|
|
7350
9449
|
}
|
|
7351
|
-
var import_cross_fetch2,
|
|
9450
|
+
var import_cross_fetch2, import_moment8, import_process, isBrowser, expiryDocID, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_TIME_FORMAT;
|
|
7352
9451
|
var init_couch = __esm({
|
|
7353
9452
|
"src/impl/couch/index.ts"() {
|
|
7354
9453
|
init_factory();
|
|
7355
9454
|
init_types_legacy();
|
|
7356
9455
|
import_cross_fetch2 = __toESM(require("cross-fetch"), 1);
|
|
7357
|
-
|
|
9456
|
+
import_moment8 = __toESM(require("moment"), 1);
|
|
7358
9457
|
init_logger();
|
|
7359
9458
|
init_pouchdb_setup();
|
|
7360
9459
|
import_process = __toESM(require("process"), 1);
|