@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/core/index.js
CHANGED
|
@@ -750,13 +750,20 @@ function captureRun(report) {
|
|
|
750
750
|
runHistory.pop();
|
|
751
751
|
}
|
|
752
752
|
}
|
|
753
|
-
function
|
|
753
|
+
function parseCardElo(provenance) {
|
|
754
|
+
const eloEntry = provenance.find((p) => p.strategy === "elo");
|
|
755
|
+
if (!eloEntry?.reason) return void 0;
|
|
756
|
+
const match = eloEntry.reason.match(/card:\s*(\d+)/);
|
|
757
|
+
return match ? parseInt(match[1], 10) : void 0;
|
|
758
|
+
}
|
|
759
|
+
function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
|
|
754
760
|
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
755
761
|
const cards = allCards.map((card) => ({
|
|
756
762
|
cardId: card.cardId,
|
|
757
763
|
courseId: card.courseId,
|
|
758
764
|
origin: getOrigin(card),
|
|
759
765
|
finalScore: card.score,
|
|
766
|
+
cardElo: parseCardElo(card.provenance),
|
|
760
767
|
provenance: card.provenance,
|
|
761
768
|
tags: card.tags,
|
|
762
769
|
selected: selectedIds.has(card.cardId)
|
|
@@ -766,6 +773,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
766
773
|
return {
|
|
767
774
|
courseId,
|
|
768
775
|
courseName,
|
|
776
|
+
userElo,
|
|
769
777
|
generatorName,
|
|
770
778
|
generators,
|
|
771
779
|
generatedCount,
|
|
@@ -786,6 +794,7 @@ function printRunSummary(run) {
|
|
|
786
794
|
console.group(`\u{1F50D} Pipeline Run: ${run.courseId} (${run.courseName || "unnamed"})`);
|
|
787
795
|
logger.info(`Run ID: ${run.runId}`);
|
|
788
796
|
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
797
|
+
logger.info(`User ELO: ${run.userElo ?? "unknown"}`);
|
|
789
798
|
logger.info(`Generator: ${run.generatorName} \u2192 ${run.generatedCount} candidates`);
|
|
790
799
|
if (run.generators && run.generators.length > 0) {
|
|
791
800
|
console.group("Generator breakdown:");
|
|
@@ -872,8 +881,12 @@ var init_PipelineDebugger = __esm({
|
|
|
872
881
|
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
873
882
|
logger.info(`Course: ${card.courseId}`);
|
|
874
883
|
logger.info(`Origin: ${card.origin}`);
|
|
884
|
+
logger.info(`Card ELO: ${card.cardElo ?? "unknown"} | User ELO: ${run.userElo ?? "unknown"}`);
|
|
875
885
|
logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
|
|
876
886
|
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
887
|
+
if (card.tags && card.tags.length > 0) {
|
|
888
|
+
logger.info(`Tags (${card.tags.length}): ${card.tags.join(", ")}`);
|
|
889
|
+
}
|
|
877
890
|
logger.info("Provenance:");
|
|
878
891
|
logger.info(formatProvenance(card.provenance));
|
|
879
892
|
console.groupEnd();
|
|
@@ -1037,6 +1050,27 @@ var init_PipelineDebugger = __esm({
|
|
|
1037
1050
|
}
|
|
1038
1051
|
return _activePipeline.diagnoseCardSpace({ threshold });
|
|
1039
1052
|
},
|
|
1053
|
+
/**
|
|
1054
|
+
* Show user's per-tag ELO data. Useful for diagnosing hierarchy gate status.
|
|
1055
|
+
*
|
|
1056
|
+
* @param tagFilter - Optional glob pattern(s) to filter tags.
|
|
1057
|
+
* Examples: 'gpc:expose:*', 'gpc:intro:t-T', ['gpc:expose:t-*', 'gpc:intro:t-*']
|
|
1058
|
+
*/
|
|
1059
|
+
async showTagElo(tagFilter) {
|
|
1060
|
+
if (!_activePipeline) {
|
|
1061
|
+
logger.info("[Pipeline Debug] No active pipeline. Run a session first.");
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
const status = await _activePipeline.getTagEloStatus(tagFilter);
|
|
1065
|
+
const entries = Object.entries(status).sort(([a], [b]) => a.localeCompare(b));
|
|
1066
|
+
if (entries.length === 0) {
|
|
1067
|
+
logger.info(`[Pipeline Debug] No tag ELO data found${tagFilter ? ` for pattern: ${tagFilter}` : ""}.`);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
console.table(
|
|
1071
|
+
Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
|
|
1072
|
+
);
|
|
1073
|
+
},
|
|
1040
1074
|
/**
|
|
1041
1075
|
* Show help.
|
|
1042
1076
|
*/
|
|
@@ -1048,6 +1082,7 @@ Commands:
|
|
|
1048
1082
|
.showLastRun() Show summary of most recent pipeline run
|
|
1049
1083
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
1050
1084
|
.showCard(cardId) Show provenance trail for a specific card
|
|
1085
|
+
.showTagElo(pattern) Show user's tag ELO data (async). E.g. 'gpc:expose:*'
|
|
1051
1086
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
1052
1087
|
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
1053
1088
|
.showRegistry() Show navigator registry (classes + roles)
|
|
@@ -1361,60 +1396,423 @@ var prescribed_exports = {};
|
|
|
1361
1396
|
__export(prescribed_exports, {
|
|
1362
1397
|
default: () => PrescribedCardsGenerator
|
|
1363
1398
|
});
|
|
1364
|
-
|
|
1399
|
+
function dedupe(arr) {
|
|
1400
|
+
return [...new Set(arr)];
|
|
1401
|
+
}
|
|
1402
|
+
function isoNow() {
|
|
1403
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1404
|
+
}
|
|
1405
|
+
function clamp(value, min, max) {
|
|
1406
|
+
return Math.max(min, Math.min(max, value));
|
|
1407
|
+
}
|
|
1408
|
+
function matchesTagPattern(tag, pattern) {
|
|
1409
|
+
if (pattern === "*") return true;
|
|
1410
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
1411
|
+
const re = new RegExp(`^${escaped}$`);
|
|
1412
|
+
return re.test(tag);
|
|
1413
|
+
}
|
|
1414
|
+
function pickTopByScore(cards, limit) {
|
|
1415
|
+
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1416
|
+
}
|
|
1417
|
+
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;
|
|
1365
1418
|
var init_prescribed = __esm({
|
|
1366
1419
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1367
1420
|
"use strict";
|
|
1368
1421
|
init_navigators();
|
|
1369
1422
|
init_logger();
|
|
1423
|
+
DEFAULT_FRESHNESS_WINDOW = 3;
|
|
1424
|
+
DEFAULT_MAX_DIRECT_PER_RUN = 3;
|
|
1425
|
+
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1426
|
+
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1427
|
+
DEFAULT_MIN_COUNT = 3;
|
|
1428
|
+
BASE_TARGET_SCORE = 1;
|
|
1429
|
+
BASE_SUPPORT_SCORE = 0.8;
|
|
1430
|
+
MAX_TARGET_MULTIPLIER = 8;
|
|
1431
|
+
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1432
|
+
LOCKED_TAG_PREFIXES = ["concept:"];
|
|
1433
|
+
LESSON_GATE_PENALTY_TAG_HINT = "concept:";
|
|
1370
1434
|
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1371
1435
|
name;
|
|
1372
1436
|
config;
|
|
1373
1437
|
constructor(user, course, strategyData) {
|
|
1374
1438
|
super(user, course, strategyData);
|
|
1375
1439
|
this.name = strategyData.name || "Prescribed Cards";
|
|
1376
|
-
|
|
1377
|
-
const parsed = JSON.parse(strategyData.serializedData);
|
|
1378
|
-
this.config = { cardIds: parsed.cardIds || [] };
|
|
1379
|
-
} catch {
|
|
1380
|
-
this.config = { cardIds: [] };
|
|
1381
|
-
}
|
|
1440
|
+
this.config = this.parseConfig(strategyData.serializedData);
|
|
1382
1441
|
logger.debug(
|
|
1383
|
-
`[Prescribed] Initialized with ${this.config.
|
|
1442
|
+
`[Prescribed] Initialized with ${this.config.groups.length} groups and ${this.config.groups.reduce((n, g) => n + g.targetCardIds.length, 0)} targets`
|
|
1384
1443
|
);
|
|
1385
1444
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1445
|
+
get strategyKey() {
|
|
1446
|
+
return "PrescribedProgress";
|
|
1447
|
+
}
|
|
1448
|
+
async getWeightedCards(limit, context) {
|
|
1449
|
+
if (this.config.groups.length === 0 || limit <= 0) {
|
|
1388
1450
|
return [];
|
|
1389
1451
|
}
|
|
1390
1452
|
const courseId = this.course.getCourseID();
|
|
1391
1453
|
const activeCards = await this.user.getActiveCards();
|
|
1392
1454
|
const activeIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1395
|
-
|
|
1455
|
+
const seenCards = await this.user.getSeenCards(courseId).catch(() => []);
|
|
1456
|
+
const seenIds = new Set(seenCards);
|
|
1457
|
+
const progress = await this.getStrategyState() ?? {
|
|
1458
|
+
updatedAt: isoNow(),
|
|
1459
|
+
groups: {}
|
|
1460
|
+
};
|
|
1461
|
+
const hierarchyConfigs = await this.loadHierarchyConfigs();
|
|
1462
|
+
const courseReg = await this.user.getCourseRegDoc(courseId).catch(() => null);
|
|
1463
|
+
const userGlobalElo = typeof courseReg?.elo === "number" ? courseReg.elo : courseReg?.elo?.global?.score ?? context?.userElo ?? 1e3;
|
|
1464
|
+
const userTagElo = typeof courseReg?.elo === "number" ? {} : courseReg?.elo?.tags ?? {};
|
|
1465
|
+
const allTargetIds = dedupe(this.config.groups.flatMap((g) => g.targetCardIds));
|
|
1466
|
+
const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
|
|
1467
|
+
const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
|
|
1468
|
+
const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
|
|
1469
|
+
const nextState = {
|
|
1470
|
+
updatedAt: isoNow(),
|
|
1471
|
+
groups: {}
|
|
1472
|
+
};
|
|
1473
|
+
const emitted = [];
|
|
1474
|
+
const emittedIds = /* @__PURE__ */ new Set();
|
|
1475
|
+
for (const group of this.config.groups) {
|
|
1476
|
+
const runtime = this.buildGroupRuntimeState({
|
|
1477
|
+
group,
|
|
1478
|
+
priorState: progress.groups[group.id],
|
|
1479
|
+
activeIds,
|
|
1480
|
+
seenIds,
|
|
1481
|
+
tagsByCard,
|
|
1482
|
+
hierarchyConfigs,
|
|
1483
|
+
userTagElo,
|
|
1484
|
+
userGlobalElo
|
|
1485
|
+
});
|
|
1486
|
+
nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
|
|
1487
|
+
const directCards = this.buildDirectTargetCards(
|
|
1488
|
+
runtime,
|
|
1489
|
+
courseId,
|
|
1490
|
+
emittedIds
|
|
1491
|
+
);
|
|
1492
|
+
const supportCards = this.buildSupportCards(
|
|
1493
|
+
runtime,
|
|
1494
|
+
courseId,
|
|
1495
|
+
emittedIds
|
|
1496
|
+
);
|
|
1497
|
+
emitted.push(...directCards, ...supportCards);
|
|
1498
|
+
}
|
|
1499
|
+
if (emitted.length === 0) {
|
|
1500
|
+
logger.debug("[Prescribed] No prescribed targets/support emitted this run");
|
|
1501
|
+
await this.putStrategyState(nextState).catch((e) => {
|
|
1502
|
+
logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
|
|
1503
|
+
});
|
|
1396
1504
|
return [];
|
|
1397
1505
|
}
|
|
1398
|
-
const
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1506
|
+
const finalCards = pickTopByScore(emitted, limit);
|
|
1507
|
+
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
1508
|
+
for (const card of finalCards) {
|
|
1509
|
+
const prov = card.provenance[0];
|
|
1510
|
+
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
1511
|
+
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
1512
|
+
if (!groupId) continue;
|
|
1513
|
+
if (!surfacedByGroup.has(groupId)) {
|
|
1514
|
+
surfacedByGroup.set(groupId, { targetIds: [], supportIds: [] });
|
|
1515
|
+
}
|
|
1516
|
+
surfacedByGroup.get(groupId)[mode].push(card.cardId);
|
|
1517
|
+
}
|
|
1518
|
+
for (const group of this.config.groups) {
|
|
1519
|
+
const groupState = nextState.groups[group.id];
|
|
1520
|
+
const surfaced = surfacedByGroup.get(group.id);
|
|
1521
|
+
if (surfaced && (surfaced.targetIds.length > 0 || surfaced.supportIds.length > 0)) {
|
|
1522
|
+
groupState.lastSurfacedAt = isoNow();
|
|
1523
|
+
groupState.sessionsSinceSurfaced = 0;
|
|
1524
|
+
if (surfaced.supportIds.length > 0) {
|
|
1525
|
+
groupState.lastSupportAt = isoNow();
|
|
1410
1526
|
}
|
|
1411
|
-
|
|
1412
|
-
}
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
await this.putStrategyState(nextState).catch((e) => {
|
|
1530
|
+
logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
|
|
1531
|
+
});
|
|
1413
1532
|
logger.info(
|
|
1414
|
-
`[Prescribed] Emitting ${
|
|
1533
|
+
`[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)`
|
|
1415
1534
|
);
|
|
1535
|
+
return finalCards;
|
|
1536
|
+
}
|
|
1537
|
+
parseConfig(serializedData) {
|
|
1538
|
+
try {
|
|
1539
|
+
const parsed = JSON.parse(serializedData);
|
|
1540
|
+
const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
|
|
1541
|
+
const groups = groupsRaw.map((raw, i) => ({
|
|
1542
|
+
id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
|
|
1543
|
+
targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
|
|
1544
|
+
supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
|
|
1545
|
+
supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
|
|
1546
|
+
freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
|
|
1547
|
+
maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
|
|
1548
|
+
maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
|
|
1549
|
+
hierarchyWalk: {
|
|
1550
|
+
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
1551
|
+
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
1552
|
+
},
|
|
1553
|
+
retireOnEncounter: raw.retireOnEncounter !== false
|
|
1554
|
+
})).filter((g) => g.targetCardIds.length > 0);
|
|
1555
|
+
return { groups };
|
|
1556
|
+
} catch {
|
|
1557
|
+
return { groups: [] };
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
async loadHierarchyConfigs() {
|
|
1561
|
+
try {
|
|
1562
|
+
const strategies = await this.course.getNavigationStrategies();
|
|
1563
|
+
return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
|
|
1564
|
+
try {
|
|
1565
|
+
const parsed = JSON.parse(s.serializedData);
|
|
1566
|
+
return {
|
|
1567
|
+
prerequisites: parsed.prerequisites || {}
|
|
1568
|
+
};
|
|
1569
|
+
} catch {
|
|
1570
|
+
return { prerequisites: {} };
|
|
1571
|
+
}
|
|
1572
|
+
});
|
|
1573
|
+
} catch (e) {
|
|
1574
|
+
logger.debug(`[Prescribed] Failed to load hierarchy configs: ${e}`);
|
|
1575
|
+
return [];
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
buildGroupRuntimeState(args) {
|
|
1579
|
+
const {
|
|
1580
|
+
group,
|
|
1581
|
+
priorState,
|
|
1582
|
+
activeIds,
|
|
1583
|
+
seenIds,
|
|
1584
|
+
tagsByCard,
|
|
1585
|
+
hierarchyConfigs,
|
|
1586
|
+
userTagElo,
|
|
1587
|
+
userGlobalElo
|
|
1588
|
+
} = args;
|
|
1589
|
+
const encounteredTargets = /* @__PURE__ */ new Set();
|
|
1590
|
+
for (const cardId of group.targetCardIds) {
|
|
1591
|
+
if (activeIds.has(cardId) || seenIds.has(cardId)) {
|
|
1592
|
+
encounteredTargets.add(cardId);
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
if (priorState?.encounteredCardIds?.length) {
|
|
1596
|
+
for (const cardId of priorState.encounteredCardIds) {
|
|
1597
|
+
encounteredTargets.add(cardId);
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
const pendingTargets = group.targetCardIds.filter((id) => !encounteredTargets.has(id));
|
|
1601
|
+
const targetTags = /* @__PURE__ */ new Map();
|
|
1602
|
+
for (const cardId of pendingTargets) {
|
|
1603
|
+
targetTags.set(cardId, tagsByCard.get(cardId) ?? []);
|
|
1604
|
+
}
|
|
1605
|
+
const blockedTargets = [];
|
|
1606
|
+
const surfaceableTargets = [];
|
|
1607
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1608
|
+
for (const cardId of pendingTargets) {
|
|
1609
|
+
const tags = targetTags.get(cardId) ?? [];
|
|
1610
|
+
const resolution = this.resolveBlockedSupportTags(
|
|
1611
|
+
tags,
|
|
1612
|
+
hierarchyConfigs,
|
|
1613
|
+
userTagElo,
|
|
1614
|
+
userGlobalElo,
|
|
1615
|
+
group.hierarchyWalk?.enabled !== false,
|
|
1616
|
+
group.hierarchyWalk?.maxDepth ?? DEFAULT_HIERARCHY_DEPTH
|
|
1617
|
+
);
|
|
1618
|
+
if (resolution.blocked) {
|
|
1619
|
+
blockedTargets.push(cardId);
|
|
1620
|
+
resolution.supportTags.forEach((t) => supportTags.add(t));
|
|
1621
|
+
} else {
|
|
1622
|
+
surfaceableTargets.push(cardId);
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
const supportCandidates = dedupe([
|
|
1626
|
+
...group.supportCardIds ?? [],
|
|
1627
|
+
...this.findSupportCardsByTags(
|
|
1628
|
+
group,
|
|
1629
|
+
tagsByCard,
|
|
1630
|
+
[...supportTags]
|
|
1631
|
+
)
|
|
1632
|
+
]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
|
|
1633
|
+
const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
|
|
1634
|
+
const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
|
|
1635
|
+
const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
|
|
1636
|
+
const pressureMultiplier = pendingTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.75 + Math.min(2, pendingTargets.length * 0.1), 1, MAX_TARGET_MULTIPLIER);
|
|
1637
|
+
const supportMultiplier = blockedTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.5 + Math.min(1.5, blockedTargets.length * 0.15), 1, MAX_SUPPORT_MULTIPLIER);
|
|
1638
|
+
return {
|
|
1639
|
+
group,
|
|
1640
|
+
encounteredTargets,
|
|
1641
|
+
pendingTargets,
|
|
1642
|
+
blockedTargets,
|
|
1643
|
+
surfaceableTargets,
|
|
1644
|
+
targetTags,
|
|
1645
|
+
supportCandidates,
|
|
1646
|
+
supportTags: [...supportTags],
|
|
1647
|
+
pressureMultiplier,
|
|
1648
|
+
supportMultiplier
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
buildNextGroupState(runtime, prior) {
|
|
1652
|
+
const carriedSessions = prior?.sessionsSinceSurfaced ?? 0;
|
|
1653
|
+
const surfacedThisRun = false;
|
|
1654
|
+
return {
|
|
1655
|
+
encounteredCardIds: [...runtime.encounteredTargets].sort(),
|
|
1656
|
+
lastSurfacedAt: prior?.lastSurfacedAt ?? null,
|
|
1657
|
+
sessionsSinceSurfaced: surfacedThisRun ? 0 : carriedSessions + 1,
|
|
1658
|
+
lastSupportAt: prior?.lastSupportAt ?? null,
|
|
1659
|
+
blockedTargetIds: [...runtime.blockedTargets].sort(),
|
|
1660
|
+
lastResolvedSupportTags: [...runtime.supportTags].sort()
|
|
1661
|
+
};
|
|
1662
|
+
}
|
|
1663
|
+
buildDirectTargetCards(runtime, courseId, emittedIds) {
|
|
1664
|
+
const maxDirect = runtime.group.maxDirectTargetsPerRun ?? DEFAULT_MAX_DIRECT_PER_RUN;
|
|
1665
|
+
const directIds = runtime.surfaceableTargets.filter((id) => !emittedIds.has(id)).slice(0, maxDirect);
|
|
1666
|
+
const cards = [];
|
|
1667
|
+
for (const cardId of directIds) {
|
|
1668
|
+
emittedIds.add(cardId);
|
|
1669
|
+
cards.push({
|
|
1670
|
+
cardId,
|
|
1671
|
+
courseId,
|
|
1672
|
+
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1673
|
+
provenance: [
|
|
1674
|
+
{
|
|
1675
|
+
strategy: "prescribed",
|
|
1676
|
+
strategyName: this.strategyName || this.name,
|
|
1677
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1678
|
+
action: "generated",
|
|
1679
|
+
score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
|
|
1680
|
+
reason: `mode=target;group=${runtime.group.id};pending=${runtime.pendingTargets.length};surfaceable=${runtime.surfaceableTargets.length};blocked=${runtime.blockedTargets.length};multiplier=${runtime.pressureMultiplier.toFixed(2)}`
|
|
1681
|
+
}
|
|
1682
|
+
]
|
|
1683
|
+
});
|
|
1684
|
+
}
|
|
1685
|
+
return cards;
|
|
1686
|
+
}
|
|
1687
|
+
buildSupportCards(runtime, courseId, emittedIds) {
|
|
1688
|
+
if (runtime.blockedTargets.length === 0 || runtime.supportCandidates.length === 0) {
|
|
1689
|
+
return [];
|
|
1690
|
+
}
|
|
1691
|
+
const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
|
|
1692
|
+
const supportIds = runtime.supportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
|
|
1693
|
+
const cards = [];
|
|
1694
|
+
for (const cardId of supportIds) {
|
|
1695
|
+
emittedIds.add(cardId);
|
|
1696
|
+
cards.push({
|
|
1697
|
+
cardId,
|
|
1698
|
+
courseId,
|
|
1699
|
+
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1700
|
+
provenance: [
|
|
1701
|
+
{
|
|
1702
|
+
strategy: "prescribed",
|
|
1703
|
+
strategyName: this.strategyName || this.name,
|
|
1704
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1705
|
+
action: "generated",
|
|
1706
|
+
score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
|
|
1707
|
+
reason: `mode=support;group=${runtime.group.id};blocked=${runtime.blockedTargets.length};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.supportMultiplier.toFixed(2)}`
|
|
1708
|
+
}
|
|
1709
|
+
]
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1416
1712
|
return cards;
|
|
1417
1713
|
}
|
|
1714
|
+
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
1715
|
+
if (supportTags.length === 0) {
|
|
1716
|
+
return [];
|
|
1717
|
+
}
|
|
1718
|
+
const explicitSupportIds = group.supportCardIds ?? [];
|
|
1719
|
+
const explicitPatterns = group.supportTagPatterns ?? [];
|
|
1720
|
+
if (explicitSupportIds.length === 0 && explicitPatterns.length === 0) {
|
|
1721
|
+
return [];
|
|
1722
|
+
}
|
|
1723
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
1724
|
+
for (const cardId of explicitSupportIds) {
|
|
1725
|
+
const cardTags = tagsByCard.get(cardId) ?? [];
|
|
1726
|
+
const matchesResolved = supportTags.some((supportTag) => cardTags.includes(supportTag));
|
|
1727
|
+
const matchesPattern = explicitPatterns.some(
|
|
1728
|
+
(pattern) => cardTags.some((tag) => matchesTagPattern(tag, pattern))
|
|
1729
|
+
);
|
|
1730
|
+
if (matchesResolved || matchesPattern) {
|
|
1731
|
+
candidates.add(cardId);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
return [...candidates];
|
|
1735
|
+
}
|
|
1736
|
+
resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
|
|
1737
|
+
if (!hierarchyWalkEnabled || targetTags.length === 0 || hierarchyConfigs.length === 0) {
|
|
1738
|
+
return {
|
|
1739
|
+
blocked: false,
|
|
1740
|
+
supportTags: []
|
|
1741
|
+
};
|
|
1742
|
+
}
|
|
1743
|
+
const supportTags = /* @__PURE__ */ new Set();
|
|
1744
|
+
let blocked = false;
|
|
1745
|
+
for (const targetTag of targetTags) {
|
|
1746
|
+
for (const hierarchy of hierarchyConfigs) {
|
|
1747
|
+
const prereqs = hierarchy.prerequisites[targetTag];
|
|
1748
|
+
if (!prereqs || prereqs.length === 0) continue;
|
|
1749
|
+
const unmet = prereqs.filter(
|
|
1750
|
+
(pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo)
|
|
1751
|
+
);
|
|
1752
|
+
if (unmet.length === 0) {
|
|
1753
|
+
continue;
|
|
1754
|
+
}
|
|
1755
|
+
blocked = true;
|
|
1756
|
+
for (const prereq of unmet) {
|
|
1757
|
+
this.collectSupportTagsRecursive(
|
|
1758
|
+
prereq.tag,
|
|
1759
|
+
hierarchyConfigs,
|
|
1760
|
+
userTagElo,
|
|
1761
|
+
userGlobalElo,
|
|
1762
|
+
maxDepth,
|
|
1763
|
+
/* @__PURE__ */ new Set(),
|
|
1764
|
+
supportTags
|
|
1765
|
+
);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
return { blocked, supportTags: [...supportTags] };
|
|
1770
|
+
}
|
|
1771
|
+
collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
|
|
1772
|
+
if (depth < 0 || visited.has(tag)) return;
|
|
1773
|
+
if (this.isHardGatedTag(tag)) return;
|
|
1774
|
+
visited.add(tag);
|
|
1775
|
+
let walkedFurther = false;
|
|
1776
|
+
for (const hierarchy of hierarchyConfigs) {
|
|
1777
|
+
const prereqs = hierarchy.prerequisites[tag];
|
|
1778
|
+
if (!prereqs || prereqs.length === 0) continue;
|
|
1779
|
+
const unmet = prereqs.filter(
|
|
1780
|
+
(pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo)
|
|
1781
|
+
);
|
|
1782
|
+
if (unmet.length > 0 && depth > 0) {
|
|
1783
|
+
walkedFurther = true;
|
|
1784
|
+
for (const prereq of unmet) {
|
|
1785
|
+
this.collectSupportTagsRecursive(
|
|
1786
|
+
prereq.tag,
|
|
1787
|
+
hierarchyConfigs,
|
|
1788
|
+
userTagElo,
|
|
1789
|
+
userGlobalElo,
|
|
1790
|
+
depth - 1,
|
|
1791
|
+
visited,
|
|
1792
|
+
out
|
|
1793
|
+
);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
if (!walkedFurther) {
|
|
1798
|
+
out.add(tag);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
isHardGatedTag(tag) {
|
|
1802
|
+
return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
|
|
1803
|
+
}
|
|
1804
|
+
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
1805
|
+
if (!userTagElo) return false;
|
|
1806
|
+
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
|
|
1807
|
+
if (userTagElo.count < minCount) return false;
|
|
1808
|
+
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1809
|
+
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
1810
|
+
}
|
|
1811
|
+
if (prereq.masteryThreshold?.minCount !== void 0) {
|
|
1812
|
+
return true;
|
|
1813
|
+
}
|
|
1814
|
+
return userTagElo.score >= userGlobalElo;
|
|
1815
|
+
}
|
|
1418
1816
|
};
|
|
1419
1817
|
}
|
|
1420
1818
|
});
|
|
@@ -1777,13 +2175,14 @@ var hierarchyDefinition_exports = {};
|
|
|
1777
2175
|
__export(hierarchyDefinition_exports, {
|
|
1778
2176
|
default: () => HierarchyDefinitionNavigator
|
|
1779
2177
|
});
|
|
1780
|
-
var import_common6,
|
|
2178
|
+
var import_common6, DEFAULT_MIN_COUNT2, HierarchyDefinitionNavigator;
|
|
1781
2179
|
var init_hierarchyDefinition = __esm({
|
|
1782
2180
|
"src/core/navigators/filters/hierarchyDefinition.ts"() {
|
|
1783
2181
|
"use strict";
|
|
1784
2182
|
init_navigators();
|
|
1785
2183
|
import_common6 = require("@vue-skuilder/common");
|
|
1786
|
-
|
|
2184
|
+
init_logger();
|
|
2185
|
+
DEFAULT_MIN_COUNT2 = 3;
|
|
1787
2186
|
HierarchyDefinitionNavigator = class extends ContentNavigator {
|
|
1788
2187
|
config;
|
|
1789
2188
|
/** Human-readable name for CardFilter interface */
|
|
@@ -1810,7 +2209,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1810
2209
|
*/
|
|
1811
2210
|
isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
|
|
1812
2211
|
if (!userTagElo) return false;
|
|
1813
|
-
const minCount = prereq.masteryThreshold?.minCount ??
|
|
2212
|
+
const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT2;
|
|
1814
2213
|
if (userTagElo.count < minCount) return false;
|
|
1815
2214
|
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1816
2215
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
@@ -1911,18 +2310,55 @@ var init_hierarchyDefinition = __esm({
|
|
|
1911
2310
|
}
|
|
1912
2311
|
return boosts;
|
|
1913
2312
|
}
|
|
2313
|
+
/**
|
|
2314
|
+
* Build a map of gated tag → max configured targetBoost for all *open* gates.
|
|
2315
|
+
*
|
|
2316
|
+
* When a gate opens (prereqs met), cards carrying the gated tag get boosted —
|
|
2317
|
+
* ensuring newly-unlocked content surfaces promptly. The boost is a static
|
|
2318
|
+
* multiplier; natural ELO/SRS deprioritization after interaction handles decay.
|
|
2319
|
+
*/
|
|
2320
|
+
getTargetBoosts(unlockedTags) {
|
|
2321
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
2322
|
+
const configKeys = Object.keys(this.config.prerequisites);
|
|
2323
|
+
const unlockedArr = [...unlockedTags];
|
|
2324
|
+
logger.info(
|
|
2325
|
+
`[HierarchyDefinition:targetBoost:trace] ${this.name} | configKeys=${configKeys.length}, unlocked=${unlockedArr.length} (${unlockedArr.slice(0, 5).join(", ")}${unlockedArr.length > 5 ? "..." : ""})`
|
|
2326
|
+
);
|
|
2327
|
+
for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
|
|
2328
|
+
if (!unlockedTags.has(tagId)) continue;
|
|
2329
|
+
logger.info(
|
|
2330
|
+
`[HierarchyDefinition:targetBoost:trace] UNLOCKED ${tagId}: ${prereqs.length} prereqs, raw=${JSON.stringify(prereqs.map((p) => ({ tag: p.tag, tb: p.targetBoost })))}`
|
|
2331
|
+
);
|
|
2332
|
+
for (const prereq of prereqs) {
|
|
2333
|
+
if (!prereq.targetBoost || prereq.targetBoost <= 1) continue;
|
|
2334
|
+
const existing = boosts.get(tagId) ?? 1;
|
|
2335
|
+
boosts.set(tagId, Math.max(existing, prereq.targetBoost));
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
if (boosts.size > 0) {
|
|
2339
|
+
logger.info(
|
|
2340
|
+
`[HierarchyDefinition] targetBoosts active: ${[...boosts.entries()].map(([t, b]) => `${t}=\xD7${b}`).join(", ")}`
|
|
2341
|
+
);
|
|
2342
|
+
} else {
|
|
2343
|
+
logger.info(
|
|
2344
|
+
`[HierarchyDefinition:targetBoost:trace] no targetBoosts found despite ${unlockedArr.length} unlocked tags`
|
|
2345
|
+
);
|
|
2346
|
+
}
|
|
2347
|
+
return boosts;
|
|
2348
|
+
}
|
|
1914
2349
|
/**
|
|
1915
2350
|
* CardFilter.transform implementation.
|
|
1916
2351
|
*
|
|
1917
|
-
*
|
|
1918
|
-
* 1. Cards with locked tags receive score * 0.
|
|
1919
|
-
* 2. Cards carrying prereq tags of closed gates receive
|
|
1920
|
-
*
|
|
2352
|
+
* Three effects:
|
|
2353
|
+
* 1. Cards with locked tags receive score * 0.02 (gating penalty)
|
|
2354
|
+
* 2. Cards carrying prereq tags of closed gates receive preReqBoost
|
|
2355
|
+
* 3. Cards carrying gated tags of open gates receive targetBoost
|
|
1921
2356
|
*/
|
|
1922
2357
|
async transform(cards, context) {
|
|
1923
2358
|
const masteredTags = await this.getMasteredTags(context);
|
|
1924
2359
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
1925
2360
|
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
2361
|
+
const targetBoosts = this.getTargetBoosts(unlockedTags);
|
|
1926
2362
|
const gated = [];
|
|
1927
2363
|
for (const card of cards) {
|
|
1928
2364
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
@@ -1950,6 +2386,29 @@ var init_hierarchyDefinition = __esm({
|
|
|
1950
2386
|
finalScore *= maxBoost;
|
|
1951
2387
|
action = "boosted";
|
|
1952
2388
|
finalReason = `${reason} | preReqBoost \xD7${maxBoost.toFixed(2)} for ${boostedPrereqs.join(", ")}`;
|
|
2389
|
+
logger.info(
|
|
2390
|
+
`[HierarchyDefinition] preReqBoost \xD7${maxBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedPrereqs.join(", ")}] (score: ${card.score.toFixed(3)} \u2192 ${finalScore.toFixed(3)})`
|
|
2391
|
+
);
|
|
2392
|
+
}
|
|
2393
|
+
}
|
|
2394
|
+
if (isUnlocked && targetBoosts.size > 0) {
|
|
2395
|
+
const cardTags = card.tags ?? [];
|
|
2396
|
+
let maxTargetBoost = 1;
|
|
2397
|
+
const boostedTargets = [];
|
|
2398
|
+
for (const tag of cardTags) {
|
|
2399
|
+
const boost = targetBoosts.get(tag);
|
|
2400
|
+
if (boost && boost > maxTargetBoost) {
|
|
2401
|
+
maxTargetBoost = boost;
|
|
2402
|
+
boostedTargets.push(tag);
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
if (maxTargetBoost > 1) {
|
|
2406
|
+
finalScore *= maxTargetBoost;
|
|
2407
|
+
action = "boosted";
|
|
2408
|
+
finalReason = `${finalReason} | targetBoost \xD7${maxTargetBoost.toFixed(2)} for ${boostedTargets.join(", ")}`;
|
|
2409
|
+
logger.info(
|
|
2410
|
+
`[HierarchyDefinition] targetBoost \xD7${maxTargetBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedTargets.join(", ")}] (score: ${card.score.toFixed(3)} \u2192 ${finalScore.toFixed(3)})`
|
|
2411
|
+
);
|
|
1953
2412
|
}
|
|
1954
2413
|
}
|
|
1955
2414
|
gated.push({
|
|
@@ -2136,13 +2595,13 @@ var interferenceMitigator_exports = {};
|
|
|
2136
2595
|
__export(interferenceMitigator_exports, {
|
|
2137
2596
|
default: () => InterferenceMitigatorNavigator
|
|
2138
2597
|
});
|
|
2139
|
-
var import_common7,
|
|
2598
|
+
var import_common7, DEFAULT_MIN_COUNT3, DEFAULT_MIN_ELAPSED_DAYS, DEFAULT_INTERFERENCE_DECAY, InterferenceMitigatorNavigator;
|
|
2140
2599
|
var init_interferenceMitigator = __esm({
|
|
2141
2600
|
"src/core/navigators/filters/interferenceMitigator.ts"() {
|
|
2142
2601
|
"use strict";
|
|
2143
2602
|
init_navigators();
|
|
2144
2603
|
import_common7 = require("@vue-skuilder/common");
|
|
2145
|
-
|
|
2604
|
+
DEFAULT_MIN_COUNT3 = 10;
|
|
2146
2605
|
DEFAULT_MIN_ELAPSED_DAYS = 3;
|
|
2147
2606
|
DEFAULT_INTERFERENCE_DECAY = 0.8;
|
|
2148
2607
|
InterferenceMitigatorNavigator = class extends ContentNavigator {
|
|
@@ -2167,7 +2626,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2167
2626
|
return {
|
|
2168
2627
|
interferenceSets: sets,
|
|
2169
2628
|
maturityThreshold: {
|
|
2170
|
-
minCount: parsed.maturityThreshold?.minCount ??
|
|
2629
|
+
minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3,
|
|
2171
2630
|
minElo: parsed.maturityThreshold?.minElo,
|
|
2172
2631
|
minElapsedDays: parsed.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS
|
|
2173
2632
|
},
|
|
@@ -2177,7 +2636,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2177
2636
|
return {
|
|
2178
2637
|
interferenceSets: [],
|
|
2179
2638
|
maturityThreshold: {
|
|
2180
|
-
minCount:
|
|
2639
|
+
minCount: DEFAULT_MIN_COUNT3,
|
|
2181
2640
|
minElapsedDays: DEFAULT_MIN_ELAPSED_DAYS
|
|
2182
2641
|
},
|
|
2183
2642
|
defaultDecay: DEFAULT_INTERFERENCE_DECAY
|
|
@@ -2224,7 +2683,7 @@ var init_interferenceMitigator = __esm({
|
|
|
2224
2683
|
try {
|
|
2225
2684
|
const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
|
|
2226
2685
|
const userElo = (0, import_common7.toCourseElo)(courseReg.elo);
|
|
2227
|
-
const minCount = this.config.maturityThreshold?.minCount ??
|
|
2686
|
+
const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3;
|
|
2228
2687
|
const minElo = this.config.maturityThreshold?.minElo;
|
|
2229
2688
|
const minElapsedDays = this.config.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS;
|
|
2230
2689
|
const minCountForElapsed = minElapsedDays * 2;
|
|
@@ -2459,7 +2918,7 @@ var init_relativePriority = __esm({
|
|
|
2459
2918
|
const cardTags = card.tags ?? [];
|
|
2460
2919
|
const priority = this.computeCardPriority(cardTags);
|
|
2461
2920
|
const boostFactor = this.computeBoostFactor(priority);
|
|
2462
|
-
const finalScore = Math.max(0,
|
|
2921
|
+
const finalScore = Math.max(0, card.score * boostFactor);
|
|
2463
2922
|
const action = boostFactor > 1 ? "boosted" : boostFactor < 1 ? "penalized" : "passed";
|
|
2464
2923
|
const reason = this.buildPriorityReason(cardTags, priority, boostFactor, finalScore);
|
|
2465
2924
|
return {
|
|
@@ -2735,160 +3194,1714 @@ var init_learning = __esm({
|
|
|
2735
3194
|
}
|
|
2736
3195
|
});
|
|
2737
3196
|
|
|
2738
|
-
// src/core/orchestration/signal.ts
|
|
2739
|
-
function computeOutcomeSignal(records, config = {}) {
|
|
2740
|
-
if (!records || records.length === 0) {
|
|
2741
|
-
return null;
|
|
3197
|
+
// src/core/orchestration/signal.ts
|
|
3198
|
+
function computeOutcomeSignal(records, config = {}) {
|
|
3199
|
+
if (!records || records.length === 0) {
|
|
3200
|
+
return null;
|
|
3201
|
+
}
|
|
3202
|
+
const target = config.targetAccuracy ?? 0.85;
|
|
3203
|
+
const tolerance = config.tolerance ?? 0.05;
|
|
3204
|
+
let correct = 0;
|
|
3205
|
+
for (const r of records) {
|
|
3206
|
+
if (r.isCorrect) correct++;
|
|
3207
|
+
}
|
|
3208
|
+
const accuracy = correct / records.length;
|
|
3209
|
+
return scoreAccuracyInZone(accuracy, target, tolerance);
|
|
3210
|
+
}
|
|
3211
|
+
function scoreAccuracyInZone(accuracy, target, tolerance) {
|
|
3212
|
+
const dist = Math.abs(accuracy - target);
|
|
3213
|
+
if (dist <= tolerance) {
|
|
3214
|
+
return 1;
|
|
3215
|
+
}
|
|
3216
|
+
const excess = dist - tolerance;
|
|
3217
|
+
const slope = 2.5;
|
|
3218
|
+
return Math.max(0, 1 - excess * slope);
|
|
3219
|
+
}
|
|
3220
|
+
var init_signal = __esm({
|
|
3221
|
+
"src/core/orchestration/signal.ts"() {
|
|
3222
|
+
"use strict";
|
|
3223
|
+
}
|
|
3224
|
+
});
|
|
3225
|
+
|
|
3226
|
+
// src/core/orchestration/recording.ts
|
|
3227
|
+
async function recordUserOutcome(context, periodStart, periodEnd, records, activeStrategyIds, eloStart = 0, eloEnd = 0, config) {
|
|
3228
|
+
const { user, course, userId } = context;
|
|
3229
|
+
const courseId = course.getCourseID();
|
|
3230
|
+
const outcomeValue = computeOutcomeSignal(records, config);
|
|
3231
|
+
if (outcomeValue === null) {
|
|
3232
|
+
logger.debug(
|
|
3233
|
+
`[Orchestration] No outcome signal computed for ${userId} (insufficient data). Skipping record.`
|
|
3234
|
+
);
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
const deviations = {};
|
|
3238
|
+
for (const strategyId of activeStrategyIds) {
|
|
3239
|
+
deviations[strategyId] = context.getDeviation(strategyId);
|
|
3240
|
+
}
|
|
3241
|
+
const id = `USER_OUTCOME::${courseId}::${userId}::${periodEnd}`;
|
|
3242
|
+
const record = {
|
|
3243
|
+
_id: id,
|
|
3244
|
+
docType: "USER_OUTCOME" /* USER_OUTCOME */,
|
|
3245
|
+
courseId,
|
|
3246
|
+
userId,
|
|
3247
|
+
periodStart,
|
|
3248
|
+
periodEnd,
|
|
3249
|
+
outcomeValue,
|
|
3250
|
+
deviations,
|
|
3251
|
+
metadata: {
|
|
3252
|
+
sessionsCount: 1,
|
|
3253
|
+
// Assumes recording is triggered per-session currently
|
|
3254
|
+
cardsSeen: records.length,
|
|
3255
|
+
eloStart,
|
|
3256
|
+
eloEnd,
|
|
3257
|
+
signalType: "accuracy_in_zone"
|
|
3258
|
+
}
|
|
3259
|
+
};
|
|
3260
|
+
try {
|
|
3261
|
+
await user.putUserOutcome(record);
|
|
3262
|
+
logger.debug(
|
|
3263
|
+
`[Orchestration] Recorded outcome ${outcomeValue.toFixed(3)} for ${userId} (doc: ${id})`
|
|
3264
|
+
);
|
|
3265
|
+
} catch (e) {
|
|
3266
|
+
logger.error(`[Orchestration] Failed to record outcome: ${e}`);
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
var init_recording = __esm({
|
|
3270
|
+
"src/core/orchestration/recording.ts"() {
|
|
3271
|
+
"use strict";
|
|
3272
|
+
init_signal();
|
|
3273
|
+
init_types_legacy();
|
|
3274
|
+
init_logger();
|
|
3275
|
+
}
|
|
3276
|
+
});
|
|
3277
|
+
|
|
3278
|
+
// src/core/orchestration/index.ts
|
|
3279
|
+
function fnv1a(str) {
|
|
3280
|
+
let hash = 2166136261;
|
|
3281
|
+
for (let i = 0; i < str.length; i++) {
|
|
3282
|
+
hash ^= str.charCodeAt(i);
|
|
3283
|
+
hash = Math.imul(hash, 16777619);
|
|
3284
|
+
}
|
|
3285
|
+
return hash >>> 0;
|
|
3286
|
+
}
|
|
3287
|
+
function computeDeviation(userId, strategyId, salt) {
|
|
3288
|
+
const input = `${userId}:${strategyId}:${salt}`;
|
|
3289
|
+
const hash = fnv1a(input);
|
|
3290
|
+
const normalized = hash / 4294967296;
|
|
3291
|
+
return normalized * 2 - 1;
|
|
3292
|
+
}
|
|
3293
|
+
function computeSpread(confidence) {
|
|
3294
|
+
const clampedConfidence = Math.max(0, Math.min(1, confidence));
|
|
3295
|
+
return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
|
|
3296
|
+
}
|
|
3297
|
+
function computeEffectiveWeight(learnable, userId, strategyId, salt) {
|
|
3298
|
+
const deviation = computeDeviation(userId, strategyId, salt);
|
|
3299
|
+
const spread = computeSpread(learnable.confidence);
|
|
3300
|
+
const adjustment = deviation * spread * learnable.weight;
|
|
3301
|
+
const effective = learnable.weight + adjustment;
|
|
3302
|
+
return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
|
|
3303
|
+
}
|
|
3304
|
+
async function createOrchestrationContext(user, course) {
|
|
3305
|
+
let courseConfig;
|
|
3306
|
+
try {
|
|
3307
|
+
courseConfig = await course.getCourseConfig();
|
|
3308
|
+
} catch (e) {
|
|
3309
|
+
logger.error(`[Orchestration] Failed to load course config: ${e}`);
|
|
3310
|
+
courseConfig = {
|
|
3311
|
+
name: "Unknown",
|
|
3312
|
+
description: "",
|
|
3313
|
+
public: false,
|
|
3314
|
+
deleted: false,
|
|
3315
|
+
creator: "",
|
|
3316
|
+
admins: [],
|
|
3317
|
+
moderators: [],
|
|
3318
|
+
dataShapes: [],
|
|
3319
|
+
questionTypes: [],
|
|
3320
|
+
orchestration: { salt: "default" }
|
|
3321
|
+
};
|
|
3322
|
+
}
|
|
3323
|
+
const userId = user.getUsername();
|
|
3324
|
+
const salt = courseConfig.orchestration?.salt || "default_salt";
|
|
3325
|
+
return {
|
|
3326
|
+
user,
|
|
3327
|
+
course,
|
|
3328
|
+
userId,
|
|
3329
|
+
courseConfig,
|
|
3330
|
+
getEffectiveWeight(strategyId, learnable) {
|
|
3331
|
+
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
3332
|
+
},
|
|
3333
|
+
getDeviation(strategyId) {
|
|
3334
|
+
return computeDeviation(userId, strategyId, salt);
|
|
3335
|
+
}
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
|
|
3339
|
+
var init_orchestration = __esm({
|
|
3340
|
+
"src/core/orchestration/index.ts"() {
|
|
3341
|
+
"use strict";
|
|
3342
|
+
init_logger();
|
|
3343
|
+
init_gradient();
|
|
3344
|
+
init_learning();
|
|
3345
|
+
init_signal();
|
|
3346
|
+
init_recording();
|
|
3347
|
+
MIN_SPREAD = 0.1;
|
|
3348
|
+
MAX_SPREAD = 0.5;
|
|
3349
|
+
MIN_WEIGHT = 0.1;
|
|
3350
|
+
MAX_WEIGHT = 3;
|
|
3351
|
+
}
|
|
3352
|
+
});
|
|
3353
|
+
|
|
3354
|
+
// src/study/SpacedRepetition.ts
|
|
3355
|
+
var import_moment4, import_common8, duration;
|
|
3356
|
+
var init_SpacedRepetition = __esm({
|
|
3357
|
+
"src/study/SpacedRepetition.ts"() {
|
|
3358
|
+
"use strict";
|
|
3359
|
+
init_util();
|
|
3360
|
+
import_moment4 = __toESM(require("moment"), 1);
|
|
3361
|
+
import_common8 = require("@vue-skuilder/common");
|
|
3362
|
+
init_logger();
|
|
3363
|
+
duration = import_moment4.default.duration;
|
|
3364
|
+
}
|
|
3365
|
+
});
|
|
3366
|
+
|
|
3367
|
+
// src/study/services/SrsService.ts
|
|
3368
|
+
var import_moment5;
|
|
3369
|
+
var init_SrsService = __esm({
|
|
3370
|
+
"src/study/services/SrsService.ts"() {
|
|
3371
|
+
"use strict";
|
|
3372
|
+
import_moment5 = __toESM(require("moment"), 1);
|
|
3373
|
+
init_couch();
|
|
3374
|
+
init_SpacedRepetition();
|
|
3375
|
+
init_logger();
|
|
3376
|
+
}
|
|
3377
|
+
});
|
|
3378
|
+
|
|
3379
|
+
// src/study/services/EloService.ts
|
|
3380
|
+
var import_common9;
|
|
3381
|
+
var init_EloService = __esm({
|
|
3382
|
+
"src/study/services/EloService.ts"() {
|
|
3383
|
+
"use strict";
|
|
3384
|
+
import_common9 = require("@vue-skuilder/common");
|
|
3385
|
+
init_logger();
|
|
3386
|
+
}
|
|
3387
|
+
});
|
|
3388
|
+
|
|
3389
|
+
// src/study/services/ResponseProcessor.ts
|
|
3390
|
+
var import_common10;
|
|
3391
|
+
var init_ResponseProcessor = __esm({
|
|
3392
|
+
"src/study/services/ResponseProcessor.ts"() {
|
|
3393
|
+
"use strict";
|
|
3394
|
+
init_core();
|
|
3395
|
+
init_logger();
|
|
3396
|
+
import_common10 = require("@vue-skuilder/common");
|
|
3397
|
+
}
|
|
3398
|
+
});
|
|
3399
|
+
|
|
3400
|
+
// src/study/services/CardHydrationService.ts
|
|
3401
|
+
var import_common11;
|
|
3402
|
+
var init_CardHydrationService = __esm({
|
|
3403
|
+
"src/study/services/CardHydrationService.ts"() {
|
|
3404
|
+
"use strict";
|
|
3405
|
+
import_common11 = require("@vue-skuilder/common");
|
|
3406
|
+
init_logger();
|
|
3407
|
+
}
|
|
3408
|
+
});
|
|
3409
|
+
|
|
3410
|
+
// src/study/ItemQueue.ts
|
|
3411
|
+
var init_ItemQueue = __esm({
|
|
3412
|
+
"src/study/ItemQueue.ts"() {
|
|
3413
|
+
"use strict";
|
|
3414
|
+
}
|
|
3415
|
+
});
|
|
3416
|
+
|
|
3417
|
+
// src/util/packer/types.ts
|
|
3418
|
+
var init_types3 = __esm({
|
|
3419
|
+
"src/util/packer/types.ts"() {
|
|
3420
|
+
"use strict";
|
|
3421
|
+
}
|
|
3422
|
+
});
|
|
3423
|
+
|
|
3424
|
+
// src/util/packer/CouchDBToStaticPacker.ts
|
|
3425
|
+
var init_CouchDBToStaticPacker = __esm({
|
|
3426
|
+
"src/util/packer/CouchDBToStaticPacker.ts"() {
|
|
3427
|
+
"use strict";
|
|
3428
|
+
init_types_legacy();
|
|
3429
|
+
init_logger();
|
|
3430
|
+
}
|
|
3431
|
+
});
|
|
3432
|
+
|
|
3433
|
+
// src/util/packer/index.ts
|
|
3434
|
+
var init_packer = __esm({
|
|
3435
|
+
"src/util/packer/index.ts"() {
|
|
3436
|
+
"use strict";
|
|
3437
|
+
init_types3();
|
|
3438
|
+
init_CouchDBToStaticPacker();
|
|
3439
|
+
}
|
|
3440
|
+
});
|
|
3441
|
+
|
|
3442
|
+
// src/util/migrator/types.ts
|
|
3443
|
+
var DEFAULT_MIGRATION_OPTIONS;
|
|
3444
|
+
var init_types4 = __esm({
|
|
3445
|
+
"src/util/migrator/types.ts"() {
|
|
3446
|
+
"use strict";
|
|
3447
|
+
DEFAULT_MIGRATION_OPTIONS = {
|
|
3448
|
+
chunkBatchSize: 100,
|
|
3449
|
+
validateRoundTrip: false,
|
|
3450
|
+
cleanupOnFailure: true,
|
|
3451
|
+
timeout: 3e5
|
|
3452
|
+
// 5 minutes
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
});
|
|
3456
|
+
|
|
3457
|
+
// src/util/migrator/FileSystemAdapter.ts
|
|
3458
|
+
var FileSystemError;
|
|
3459
|
+
var init_FileSystemAdapter = __esm({
|
|
3460
|
+
"src/util/migrator/FileSystemAdapter.ts"() {
|
|
3461
|
+
"use strict";
|
|
3462
|
+
FileSystemError = class extends Error {
|
|
3463
|
+
constructor(message, operation, filePath, cause) {
|
|
3464
|
+
super(message);
|
|
3465
|
+
this.operation = operation;
|
|
3466
|
+
this.filePath = filePath;
|
|
3467
|
+
this.cause = cause;
|
|
3468
|
+
this.name = "FileSystemError";
|
|
3469
|
+
}
|
|
3470
|
+
};
|
|
3471
|
+
}
|
|
3472
|
+
});
|
|
3473
|
+
|
|
3474
|
+
// src/util/migrator/validation.ts
|
|
3475
|
+
async function validateStaticCourse(staticPath, fs) {
|
|
3476
|
+
const validation = {
|
|
3477
|
+
valid: true,
|
|
3478
|
+
manifestExists: false,
|
|
3479
|
+
chunksExist: false,
|
|
3480
|
+
attachmentsExist: false,
|
|
3481
|
+
errors: [],
|
|
3482
|
+
warnings: []
|
|
3483
|
+
};
|
|
3484
|
+
try {
|
|
3485
|
+
if (fs) {
|
|
3486
|
+
const stats = await fs.stat(staticPath);
|
|
3487
|
+
if (!stats.isDirectory()) {
|
|
3488
|
+
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3489
|
+
validation.valid = false;
|
|
3490
|
+
return validation;
|
|
3491
|
+
}
|
|
3492
|
+
} else if (!nodeFS) {
|
|
3493
|
+
validation.errors.push("File system access not available - validation skipped");
|
|
3494
|
+
validation.valid = false;
|
|
3495
|
+
return validation;
|
|
3496
|
+
} else {
|
|
3497
|
+
const stats = await nodeFS.promises.stat(staticPath);
|
|
3498
|
+
if (!stats.isDirectory()) {
|
|
3499
|
+
validation.errors.push(`Path is not a directory: ${staticPath}`);
|
|
3500
|
+
validation.valid = false;
|
|
3501
|
+
return validation;
|
|
3502
|
+
}
|
|
3503
|
+
}
|
|
3504
|
+
let manifestPath = `${staticPath}/manifest.json`;
|
|
3505
|
+
try {
|
|
3506
|
+
if (fs) {
|
|
3507
|
+
manifestPath = fs.joinPath(staticPath, "manifest.json");
|
|
3508
|
+
if (await fs.exists(manifestPath)) {
|
|
3509
|
+
validation.manifestExists = true;
|
|
3510
|
+
const manifestContent = await fs.readFile(manifestPath);
|
|
3511
|
+
const manifest = JSON.parse(manifestContent);
|
|
3512
|
+
validation.courseId = manifest.courseId;
|
|
3513
|
+
validation.courseName = manifest.courseName;
|
|
3514
|
+
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3515
|
+
validation.errors.push("Invalid manifest structure");
|
|
3516
|
+
validation.valid = false;
|
|
3517
|
+
}
|
|
3518
|
+
} else {
|
|
3519
|
+
validation.errors.push(`Manifest not found: ${manifestPath}`);
|
|
3520
|
+
validation.valid = false;
|
|
3521
|
+
}
|
|
3522
|
+
} else {
|
|
3523
|
+
manifestPath = `${staticPath}/manifest.json`;
|
|
3524
|
+
await nodeFS.promises.access(manifestPath);
|
|
3525
|
+
validation.manifestExists = true;
|
|
3526
|
+
const manifestContent = await nodeFS.promises.readFile(manifestPath, "utf8");
|
|
3527
|
+
const manifest = JSON.parse(manifestContent);
|
|
3528
|
+
validation.courseId = manifest.courseId;
|
|
3529
|
+
validation.courseName = manifest.courseName;
|
|
3530
|
+
if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
|
|
3531
|
+
validation.errors.push("Invalid manifest structure");
|
|
3532
|
+
validation.valid = false;
|
|
3533
|
+
}
|
|
3534
|
+
}
|
|
3535
|
+
} catch (error) {
|
|
3536
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Manifest not found or invalid: ${manifestPath}`;
|
|
3537
|
+
validation.errors.push(errorMessage);
|
|
3538
|
+
validation.valid = false;
|
|
3539
|
+
}
|
|
3540
|
+
let chunksPath = `${staticPath}/chunks`;
|
|
3541
|
+
try {
|
|
3542
|
+
if (fs) {
|
|
3543
|
+
chunksPath = fs.joinPath(staticPath, "chunks");
|
|
3544
|
+
if (await fs.exists(chunksPath)) {
|
|
3545
|
+
const chunksStats = await fs.stat(chunksPath);
|
|
3546
|
+
if (chunksStats.isDirectory()) {
|
|
3547
|
+
validation.chunksExist = true;
|
|
3548
|
+
} else {
|
|
3549
|
+
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3550
|
+
validation.valid = false;
|
|
3551
|
+
}
|
|
3552
|
+
} else {
|
|
3553
|
+
validation.errors.push(`Chunks directory not found: ${chunksPath}`);
|
|
3554
|
+
validation.valid = false;
|
|
3555
|
+
}
|
|
3556
|
+
} else {
|
|
3557
|
+
chunksPath = `${staticPath}/chunks`;
|
|
3558
|
+
const chunksStats = await nodeFS.promises.stat(chunksPath);
|
|
3559
|
+
if (chunksStats.isDirectory()) {
|
|
3560
|
+
validation.chunksExist = true;
|
|
3561
|
+
} else {
|
|
3562
|
+
validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
|
|
3563
|
+
validation.valid = false;
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
} catch (error) {
|
|
3567
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Chunks directory not found: ${chunksPath}`;
|
|
3568
|
+
validation.errors.push(errorMessage);
|
|
3569
|
+
validation.valid = false;
|
|
3570
|
+
}
|
|
3571
|
+
let attachmentsPath;
|
|
3572
|
+
try {
|
|
3573
|
+
if (fs) {
|
|
3574
|
+
attachmentsPath = fs.joinPath(staticPath, "attachments");
|
|
3575
|
+
if (await fs.exists(attachmentsPath)) {
|
|
3576
|
+
const attachmentsStats = await fs.stat(attachmentsPath);
|
|
3577
|
+
if (attachmentsStats.isDirectory()) {
|
|
3578
|
+
validation.attachmentsExist = true;
|
|
3579
|
+
}
|
|
3580
|
+
} else {
|
|
3581
|
+
validation.warnings.push(
|
|
3582
|
+
`Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`
|
|
3583
|
+
);
|
|
3584
|
+
}
|
|
3585
|
+
} else {
|
|
3586
|
+
attachmentsPath = `${staticPath}/attachments`;
|
|
3587
|
+
const attachmentsStats = await nodeFS.promises.stat(attachmentsPath);
|
|
3588
|
+
if (attachmentsStats.isDirectory()) {
|
|
3589
|
+
validation.attachmentsExist = true;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
} catch (error) {
|
|
3593
|
+
attachmentsPath = attachmentsPath || `${staticPath}/attachments`;
|
|
3594
|
+
const warningMessage = error instanceof FileSystemError ? error.message : `Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`;
|
|
3595
|
+
validation.warnings.push(warningMessage);
|
|
3596
|
+
}
|
|
3597
|
+
} catch (error) {
|
|
3598
|
+
validation.errors.push(
|
|
3599
|
+
`Failed to validate static course: ${error instanceof Error ? error.message : String(error)}`
|
|
3600
|
+
);
|
|
3601
|
+
validation.valid = false;
|
|
3602
|
+
}
|
|
3603
|
+
return validation;
|
|
3604
|
+
}
|
|
3605
|
+
async function validateMigration(targetDB, expectedCounts, manifest) {
|
|
3606
|
+
const validation = {
|
|
3607
|
+
valid: true,
|
|
3608
|
+
documentCountMatch: false,
|
|
3609
|
+
attachmentIntegrity: false,
|
|
3610
|
+
viewFunctionality: false,
|
|
3611
|
+
issues: []
|
|
3612
|
+
};
|
|
3613
|
+
try {
|
|
3614
|
+
logger.info("Starting migration validation...");
|
|
3615
|
+
const actualCounts = await getActualDocumentCounts(targetDB);
|
|
3616
|
+
validation.documentCountMatch = compareDocumentCounts(
|
|
3617
|
+
expectedCounts,
|
|
3618
|
+
actualCounts,
|
|
3619
|
+
validation.issues
|
|
3620
|
+
);
|
|
3621
|
+
await validateCourseConfig(targetDB, manifest, validation.issues);
|
|
3622
|
+
validation.viewFunctionality = await validateViews(targetDB, manifest, validation.issues);
|
|
3623
|
+
validation.attachmentIntegrity = await validateAttachmentIntegrity(targetDB, validation.issues);
|
|
3624
|
+
validation.valid = validation.documentCountMatch && validation.viewFunctionality && validation.attachmentIntegrity;
|
|
3625
|
+
logger.info(`Migration validation completed. Valid: ${validation.valid}`);
|
|
3626
|
+
if (validation.issues.length > 0) {
|
|
3627
|
+
logger.info(`Validation issues: ${validation.issues.length}`);
|
|
3628
|
+
validation.issues.forEach((issue) => {
|
|
3629
|
+
if (issue.type === "error") {
|
|
3630
|
+
logger.error(`${issue.category}: ${issue.message}`);
|
|
3631
|
+
} else {
|
|
3632
|
+
logger.warn(`${issue.category}: ${issue.message}`);
|
|
3633
|
+
}
|
|
3634
|
+
});
|
|
3635
|
+
}
|
|
3636
|
+
} catch (error) {
|
|
3637
|
+
validation.valid = false;
|
|
3638
|
+
validation.issues.push({
|
|
3639
|
+
type: "error",
|
|
3640
|
+
category: "metadata",
|
|
3641
|
+
message: `Validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3642
|
+
});
|
|
3643
|
+
}
|
|
3644
|
+
return validation;
|
|
3645
|
+
}
|
|
3646
|
+
async function getActualDocumentCounts(db) {
|
|
3647
|
+
const counts = {};
|
|
3648
|
+
try {
|
|
3649
|
+
const allDocs = await db.allDocs({ include_docs: true });
|
|
3650
|
+
for (const row of allDocs.rows) {
|
|
3651
|
+
if (row.id.startsWith("_design/")) {
|
|
3652
|
+
counts["_design"] = (counts["_design"] || 0) + 1;
|
|
3653
|
+
continue;
|
|
3654
|
+
}
|
|
3655
|
+
const doc = row.doc;
|
|
3656
|
+
if (doc && doc.docType) {
|
|
3657
|
+
counts[doc.docType] = (counts[doc.docType] || 0) + 1;
|
|
3658
|
+
} else {
|
|
3659
|
+
counts["unknown"] = (counts["unknown"] || 0) + 1;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
} catch (error) {
|
|
3663
|
+
logger.error("Failed to get actual document counts:", error);
|
|
3664
|
+
}
|
|
3665
|
+
return counts;
|
|
3666
|
+
}
|
|
3667
|
+
function compareDocumentCounts(expected, actual, issues) {
|
|
3668
|
+
let countsMatch = true;
|
|
3669
|
+
for (const [docType, expectedCount] of Object.entries(expected)) {
|
|
3670
|
+
const actualCount = actual[docType] || 0;
|
|
3671
|
+
if (actualCount !== expectedCount) {
|
|
3672
|
+
countsMatch = false;
|
|
3673
|
+
issues.push({
|
|
3674
|
+
type: "error",
|
|
3675
|
+
category: "documents",
|
|
3676
|
+
message: `Document count mismatch for ${docType}: expected ${expectedCount}, got ${actualCount}`
|
|
3677
|
+
});
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
for (const [docType, actualCount] of Object.entries(actual)) {
|
|
3681
|
+
if (!expected[docType] && docType !== "_design") {
|
|
3682
|
+
issues.push({
|
|
3683
|
+
type: "warning",
|
|
3684
|
+
category: "documents",
|
|
3685
|
+
message: `Unexpected document type found: ${docType} (${actualCount} documents)`
|
|
3686
|
+
});
|
|
3687
|
+
}
|
|
3688
|
+
}
|
|
3689
|
+
return countsMatch;
|
|
3690
|
+
}
|
|
3691
|
+
async function validateCourseConfig(db, manifest, issues) {
|
|
3692
|
+
try {
|
|
3693
|
+
const courseConfig = await db.get("CourseConfig");
|
|
3694
|
+
if (!courseConfig) {
|
|
3695
|
+
issues.push({
|
|
3696
|
+
type: "error",
|
|
3697
|
+
category: "course_config",
|
|
3698
|
+
message: "CourseConfig document not found after migration"
|
|
3699
|
+
});
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
if (!courseConfig.courseID) {
|
|
3703
|
+
issues.push({
|
|
3704
|
+
type: "warning",
|
|
3705
|
+
category: "course_config",
|
|
3706
|
+
message: "CourseConfig document missing courseID field"
|
|
3707
|
+
});
|
|
3708
|
+
}
|
|
3709
|
+
if (courseConfig.courseID !== manifest.courseId) {
|
|
3710
|
+
issues.push({
|
|
3711
|
+
type: "warning",
|
|
3712
|
+
category: "course_config",
|
|
3713
|
+
message: `CourseConfig courseID mismatch: expected ${manifest.courseId}, got ${courseConfig.courseID}`
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
logger.debug("CourseConfig document validation passed");
|
|
3717
|
+
} catch (error) {
|
|
3718
|
+
if (error.status === 404) {
|
|
3719
|
+
issues.push({
|
|
3720
|
+
type: "error",
|
|
3721
|
+
category: "course_config",
|
|
3722
|
+
message: "CourseConfig document not found in database"
|
|
3723
|
+
});
|
|
3724
|
+
} else {
|
|
3725
|
+
issues.push({
|
|
3726
|
+
type: "error",
|
|
3727
|
+
category: "course_config",
|
|
3728
|
+
message: `Failed to validate CourseConfig document: ${error instanceof Error ? error.message : String(error)}`
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
async function validateViews(db, manifest, issues) {
|
|
3734
|
+
let viewsValid = true;
|
|
3735
|
+
try {
|
|
3736
|
+
for (const designDoc of manifest.designDocs) {
|
|
3737
|
+
try {
|
|
3738
|
+
const doc = await db.get(designDoc._id);
|
|
3739
|
+
if (!doc) {
|
|
3740
|
+
viewsValid = false;
|
|
3741
|
+
issues.push({
|
|
3742
|
+
type: "error",
|
|
3743
|
+
category: "views",
|
|
3744
|
+
message: `Design document not found: ${designDoc._id}`
|
|
3745
|
+
});
|
|
3746
|
+
continue;
|
|
3747
|
+
}
|
|
3748
|
+
for (const viewName of Object.keys(designDoc.views)) {
|
|
3749
|
+
try {
|
|
3750
|
+
const viewPath = `${designDoc._id}/${viewName}`;
|
|
3751
|
+
await db.query(viewPath, { limit: 1 });
|
|
3752
|
+
} catch (viewError) {
|
|
3753
|
+
viewsValid = false;
|
|
3754
|
+
issues.push({
|
|
3755
|
+
type: "error",
|
|
3756
|
+
category: "views",
|
|
3757
|
+
message: `View not accessible: ${designDoc._id}/${viewName} - ${viewError}`
|
|
3758
|
+
});
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
} catch (error) {
|
|
3762
|
+
viewsValid = false;
|
|
3763
|
+
issues.push({
|
|
3764
|
+
type: "error",
|
|
3765
|
+
category: "views",
|
|
3766
|
+
message: `Failed to validate design document ${designDoc._id}: ${error}`
|
|
3767
|
+
});
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
} catch (error) {
|
|
3771
|
+
viewsValid = false;
|
|
3772
|
+
issues.push({
|
|
3773
|
+
type: "error",
|
|
3774
|
+
category: "views",
|
|
3775
|
+
message: `View validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3776
|
+
});
|
|
3777
|
+
}
|
|
3778
|
+
return viewsValid;
|
|
3779
|
+
}
|
|
3780
|
+
async function validateAttachmentIntegrity(db, issues) {
|
|
3781
|
+
let attachmentsValid = true;
|
|
3782
|
+
try {
|
|
3783
|
+
const allDocs = await db.allDocs({
|
|
3784
|
+
include_docs: true,
|
|
3785
|
+
limit: 10
|
|
3786
|
+
// Sample first 10 documents for performance
|
|
3787
|
+
});
|
|
3788
|
+
let attachmentCount = 0;
|
|
3789
|
+
let validAttachments = 0;
|
|
3790
|
+
for (const row of allDocs.rows) {
|
|
3791
|
+
const doc = row.doc;
|
|
3792
|
+
if (doc && doc._attachments) {
|
|
3793
|
+
for (const [attachmentName, _attachmentMeta] of Object.entries(doc._attachments)) {
|
|
3794
|
+
attachmentCount++;
|
|
3795
|
+
try {
|
|
3796
|
+
const attachment = await db.getAttachment(doc._id, attachmentName);
|
|
3797
|
+
if (attachment) {
|
|
3798
|
+
validAttachments++;
|
|
3799
|
+
}
|
|
3800
|
+
} catch (attachmentError) {
|
|
3801
|
+
attachmentsValid = false;
|
|
3802
|
+
issues.push({
|
|
3803
|
+
type: "error",
|
|
3804
|
+
category: "attachments",
|
|
3805
|
+
message: `Attachment not accessible: ${doc._id}/${attachmentName} - ${attachmentError}`
|
|
3806
|
+
});
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
if (attachmentCount === 0) {
|
|
3812
|
+
issues.push({
|
|
3813
|
+
type: "warning",
|
|
3814
|
+
category: "attachments",
|
|
3815
|
+
message: "No attachments found in sampled documents"
|
|
3816
|
+
});
|
|
3817
|
+
} else {
|
|
3818
|
+
logger.info(`Validated ${validAttachments}/${attachmentCount} sampled attachments`);
|
|
3819
|
+
}
|
|
3820
|
+
} catch (error) {
|
|
3821
|
+
attachmentsValid = false;
|
|
3822
|
+
issues.push({
|
|
3823
|
+
type: "error",
|
|
3824
|
+
category: "attachments",
|
|
3825
|
+
message: `Attachment validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3826
|
+
});
|
|
3827
|
+
}
|
|
3828
|
+
return attachmentsValid;
|
|
3829
|
+
}
|
|
3830
|
+
var nodeFS;
|
|
3831
|
+
var init_validation = __esm({
|
|
3832
|
+
"src/util/migrator/validation.ts"() {
|
|
3833
|
+
"use strict";
|
|
3834
|
+
init_logger();
|
|
3835
|
+
init_FileSystemAdapter();
|
|
3836
|
+
nodeFS = null;
|
|
3837
|
+
try {
|
|
3838
|
+
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3839
|
+
nodeFS = eval("require")("fs");
|
|
3840
|
+
nodeFS.promises = nodeFS.promises || eval("require")("fs").promises;
|
|
3841
|
+
}
|
|
3842
|
+
} catch {
|
|
3843
|
+
}
|
|
3844
|
+
}
|
|
3845
|
+
});
|
|
3846
|
+
|
|
3847
|
+
// src/util/migrator/StaticToCouchDBMigrator.ts
|
|
3848
|
+
var nodeFS2, nodePath, StaticToCouchDBMigrator;
|
|
3849
|
+
var init_StaticToCouchDBMigrator = __esm({
|
|
3850
|
+
"src/util/migrator/StaticToCouchDBMigrator.ts"() {
|
|
3851
|
+
"use strict";
|
|
3852
|
+
init_logger();
|
|
3853
|
+
init_types4();
|
|
3854
|
+
init_validation();
|
|
3855
|
+
init_FileSystemAdapter();
|
|
3856
|
+
nodeFS2 = null;
|
|
3857
|
+
nodePath = null;
|
|
3858
|
+
try {
|
|
3859
|
+
if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
|
|
3860
|
+
nodeFS2 = eval("require")("fs");
|
|
3861
|
+
nodePath = eval("require")("path");
|
|
3862
|
+
nodeFS2.promises = nodeFS2.promises || eval("require")("fs").promises;
|
|
3863
|
+
}
|
|
3864
|
+
} catch {
|
|
3865
|
+
}
|
|
3866
|
+
StaticToCouchDBMigrator = class {
|
|
3867
|
+
options;
|
|
3868
|
+
progressCallback;
|
|
3869
|
+
fs;
|
|
3870
|
+
constructor(options = {}, fileSystemAdapter) {
|
|
3871
|
+
this.options = {
|
|
3872
|
+
...DEFAULT_MIGRATION_OPTIONS,
|
|
3873
|
+
...options
|
|
3874
|
+
};
|
|
3875
|
+
this.fs = fileSystemAdapter;
|
|
3876
|
+
}
|
|
3877
|
+
/**
|
|
3878
|
+
* Set a progress callback to receive updates during migration
|
|
3879
|
+
*/
|
|
3880
|
+
setProgressCallback(callback) {
|
|
3881
|
+
this.progressCallback = callback;
|
|
3882
|
+
}
|
|
3883
|
+
/**
|
|
3884
|
+
* Migrate a static course to CouchDB
|
|
3885
|
+
*/
|
|
3886
|
+
async migrateCourse(staticPath, targetDB) {
|
|
3887
|
+
const startTime = Date.now();
|
|
3888
|
+
const result = {
|
|
3889
|
+
success: false,
|
|
3890
|
+
documentsRestored: 0,
|
|
3891
|
+
attachmentsRestored: 0,
|
|
3892
|
+
designDocsRestored: 0,
|
|
3893
|
+
courseConfigRestored: 0,
|
|
3894
|
+
errors: [],
|
|
3895
|
+
warnings: [],
|
|
3896
|
+
migrationTime: 0
|
|
3897
|
+
};
|
|
3898
|
+
try {
|
|
3899
|
+
logger.info(`Starting migration from ${staticPath} to CouchDB`);
|
|
3900
|
+
this.reportProgress("manifest", 0, 1, "Validating static course...");
|
|
3901
|
+
const validation = await validateStaticCourse(staticPath, this.fs);
|
|
3902
|
+
if (!validation.valid) {
|
|
3903
|
+
result.errors.push(...validation.errors);
|
|
3904
|
+
throw new Error(`Static course validation failed: ${validation.errors.join(", ")}`);
|
|
3905
|
+
}
|
|
3906
|
+
result.warnings.push(...validation.warnings);
|
|
3907
|
+
this.reportProgress("manifest", 1, 1, "Loading course manifest...");
|
|
3908
|
+
const manifest = await this.loadManifest(staticPath);
|
|
3909
|
+
logger.info(`Loaded manifest for course: ${manifest.courseId} (${manifest.courseName})`);
|
|
3910
|
+
this.reportProgress(
|
|
3911
|
+
"design_docs",
|
|
3912
|
+
0,
|
|
3913
|
+
manifest.designDocs.length,
|
|
3914
|
+
"Restoring design documents..."
|
|
3915
|
+
);
|
|
3916
|
+
const designDocResults = await this.restoreDesignDocuments(manifest.designDocs, targetDB);
|
|
3917
|
+
result.designDocsRestored = designDocResults.restored;
|
|
3918
|
+
result.errors.push(...designDocResults.errors);
|
|
3919
|
+
result.warnings.push(...designDocResults.warnings);
|
|
3920
|
+
this.reportProgress("course_config", 0, 1, "Restoring CourseConfig document...");
|
|
3921
|
+
const courseConfigResults = await this.restoreCourseConfig(manifest, targetDB);
|
|
3922
|
+
result.courseConfigRestored = courseConfigResults.restored;
|
|
3923
|
+
result.errors.push(...courseConfigResults.errors);
|
|
3924
|
+
result.warnings.push(...courseConfigResults.warnings);
|
|
3925
|
+
this.reportProgress("course_config", 1, 1, "CourseConfig document restored");
|
|
3926
|
+
const expectedCounts = this.calculateExpectedCounts(manifest);
|
|
3927
|
+
this.reportProgress(
|
|
3928
|
+
"documents",
|
|
3929
|
+
0,
|
|
3930
|
+
manifest.documentCount,
|
|
3931
|
+
"Aggregating documents from chunks..."
|
|
3932
|
+
);
|
|
3933
|
+
const documents = await this.aggregateDocuments(staticPath, manifest);
|
|
3934
|
+
const filteredDocuments = documents.filter((doc) => doc._id !== "CourseConfig");
|
|
3935
|
+
if (documents.length !== filteredDocuments.length) {
|
|
3936
|
+
result.warnings.push(
|
|
3937
|
+
`Filtered out ${documents.length - filteredDocuments.length} CourseConfig document(s) from chunks to prevent conflicts`
|
|
3938
|
+
);
|
|
3939
|
+
}
|
|
3940
|
+
this.reportProgress(
|
|
3941
|
+
"documents",
|
|
3942
|
+
filteredDocuments.length,
|
|
3943
|
+
manifest.documentCount,
|
|
3944
|
+
"Uploading documents to CouchDB..."
|
|
3945
|
+
);
|
|
3946
|
+
const docResults = await this.uploadDocuments(filteredDocuments, targetDB);
|
|
3947
|
+
result.documentsRestored = docResults.restored;
|
|
3948
|
+
result.errors.push(...docResults.errors);
|
|
3949
|
+
result.warnings.push(...docResults.warnings);
|
|
3950
|
+
const docsWithAttachments = documents.filter(
|
|
3951
|
+
(doc) => doc._attachments && Object.keys(doc._attachments).length > 0
|
|
3952
|
+
);
|
|
3953
|
+
this.reportProgress("attachments", 0, docsWithAttachments.length, "Uploading attachments...");
|
|
3954
|
+
const attachmentResults = await this.uploadAttachments(
|
|
3955
|
+
staticPath,
|
|
3956
|
+
docsWithAttachments,
|
|
3957
|
+
targetDB
|
|
3958
|
+
);
|
|
3959
|
+
result.attachmentsRestored = attachmentResults.restored;
|
|
3960
|
+
result.errors.push(...attachmentResults.errors);
|
|
3961
|
+
result.warnings.push(...attachmentResults.warnings);
|
|
3962
|
+
if (this.options.validateRoundTrip) {
|
|
3963
|
+
this.reportProgress("validation", 0, 1, "Validating migration...");
|
|
3964
|
+
const validationResult = await validateMigration(targetDB, expectedCounts, manifest);
|
|
3965
|
+
if (!validationResult.valid) {
|
|
3966
|
+
result.warnings.push("Migration validation found issues");
|
|
3967
|
+
validationResult.issues.forEach((issue) => {
|
|
3968
|
+
if (issue.type === "error") {
|
|
3969
|
+
result.errors.push(`Validation: ${issue.message}`);
|
|
3970
|
+
} else {
|
|
3971
|
+
result.warnings.push(`Validation: ${issue.message}`);
|
|
3972
|
+
}
|
|
3973
|
+
});
|
|
3974
|
+
}
|
|
3975
|
+
this.reportProgress("validation", 1, 1, "Migration validation completed");
|
|
3976
|
+
}
|
|
3977
|
+
result.success = result.errors.length === 0;
|
|
3978
|
+
result.migrationTime = Date.now() - startTime;
|
|
3979
|
+
logger.info(`Migration completed in ${result.migrationTime}ms`);
|
|
3980
|
+
logger.info(`Documents restored: ${result.documentsRestored}`);
|
|
3981
|
+
logger.info(`Attachments restored: ${result.attachmentsRestored}`);
|
|
3982
|
+
logger.info(`Design docs restored: ${result.designDocsRestored}`);
|
|
3983
|
+
logger.info(`CourseConfig restored: ${result.courseConfigRestored}`);
|
|
3984
|
+
if (result.errors.length > 0) {
|
|
3985
|
+
logger.error(`Migration completed with ${result.errors.length} errors`);
|
|
3986
|
+
}
|
|
3987
|
+
if (result.warnings.length > 0) {
|
|
3988
|
+
logger.warn(`Migration completed with ${result.warnings.length} warnings`);
|
|
3989
|
+
}
|
|
3990
|
+
} catch (error) {
|
|
3991
|
+
result.success = false;
|
|
3992
|
+
result.migrationTime = Date.now() - startTime;
|
|
3993
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3994
|
+
result.errors.push(`Migration failed: ${errorMessage}`);
|
|
3995
|
+
logger.error("Migration failed:", error);
|
|
3996
|
+
if (this.options.cleanupOnFailure) {
|
|
3997
|
+
try {
|
|
3998
|
+
await this.cleanupFailedMigration(targetDB);
|
|
3999
|
+
} catch (cleanupError) {
|
|
4000
|
+
logger.error("Failed to cleanup after migration failure:", cleanupError);
|
|
4001
|
+
result.warnings.push("Failed to cleanup after migration failure");
|
|
4002
|
+
}
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
return result;
|
|
4006
|
+
}
|
|
4007
|
+
/**
|
|
4008
|
+
* Load and parse the manifest file
|
|
4009
|
+
*/
|
|
4010
|
+
async loadManifest(staticPath) {
|
|
4011
|
+
try {
|
|
4012
|
+
let manifestContent;
|
|
4013
|
+
let manifestPath;
|
|
4014
|
+
if (this.fs) {
|
|
4015
|
+
manifestPath = this.fs.joinPath(staticPath, "manifest.json");
|
|
4016
|
+
manifestContent = await this.fs.readFile(manifestPath);
|
|
4017
|
+
} else {
|
|
4018
|
+
manifestPath = nodeFS2 && nodePath ? nodePath.join(staticPath, "manifest.json") : `${staticPath}/manifest.json`;
|
|
4019
|
+
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
4020
|
+
manifestContent = await nodeFS2.promises.readFile(manifestPath, "utf8");
|
|
4021
|
+
} else {
|
|
4022
|
+
const response = await fetch(manifestPath);
|
|
4023
|
+
if (!response.ok) {
|
|
4024
|
+
throw new Error(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
|
|
4025
|
+
}
|
|
4026
|
+
manifestContent = await response.text();
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
const manifest = JSON.parse(manifestContent);
|
|
4030
|
+
if (!manifest.version || !manifest.courseId || !manifest.chunks) {
|
|
4031
|
+
throw new Error("Invalid manifest structure");
|
|
4032
|
+
}
|
|
4033
|
+
return manifest;
|
|
4034
|
+
} catch (error) {
|
|
4035
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load manifest: ${error instanceof Error ? error.message : String(error)}`;
|
|
4036
|
+
throw new Error(errorMessage);
|
|
4037
|
+
}
|
|
4038
|
+
}
|
|
4039
|
+
/**
|
|
4040
|
+
* Restore design documents to CouchDB
|
|
4041
|
+
*/
|
|
4042
|
+
async restoreDesignDocuments(designDocs, db) {
|
|
4043
|
+
const result = { restored: 0, errors: [], warnings: [] };
|
|
4044
|
+
for (let i = 0; i < designDocs.length; i++) {
|
|
4045
|
+
const designDoc = designDocs[i];
|
|
4046
|
+
this.reportProgress("design_docs", i, designDocs.length, `Restoring ${designDoc._id}...`);
|
|
4047
|
+
try {
|
|
4048
|
+
let existingDoc;
|
|
4049
|
+
try {
|
|
4050
|
+
existingDoc = await db.get(designDoc._id);
|
|
4051
|
+
} catch {
|
|
4052
|
+
}
|
|
4053
|
+
const docToInsert = {
|
|
4054
|
+
_id: designDoc._id,
|
|
4055
|
+
views: designDoc.views
|
|
4056
|
+
};
|
|
4057
|
+
if (existingDoc) {
|
|
4058
|
+
docToInsert._rev = existingDoc._rev;
|
|
4059
|
+
logger.debug(`Updating existing design document: ${designDoc._id}`);
|
|
4060
|
+
} else {
|
|
4061
|
+
logger.debug(`Creating new design document: ${designDoc._id}`);
|
|
4062
|
+
}
|
|
4063
|
+
await db.put(docToInsert);
|
|
4064
|
+
result.restored++;
|
|
4065
|
+
} catch (error) {
|
|
4066
|
+
const errorMessage = `Failed to restore design document ${designDoc._id}: ${error instanceof Error ? error.message : String(error)}`;
|
|
4067
|
+
result.errors.push(errorMessage);
|
|
4068
|
+
logger.error(errorMessage);
|
|
4069
|
+
}
|
|
4070
|
+
}
|
|
4071
|
+
this.reportProgress(
|
|
4072
|
+
"design_docs",
|
|
4073
|
+
designDocs.length,
|
|
4074
|
+
designDocs.length,
|
|
4075
|
+
`Restored ${result.restored} design documents`
|
|
4076
|
+
);
|
|
4077
|
+
return result;
|
|
4078
|
+
}
|
|
4079
|
+
/**
|
|
4080
|
+
* Aggregate documents from all chunks
|
|
4081
|
+
*/
|
|
4082
|
+
async aggregateDocuments(staticPath, manifest) {
|
|
4083
|
+
const allDocuments = [];
|
|
4084
|
+
const documentMap = /* @__PURE__ */ new Map();
|
|
4085
|
+
for (let i = 0; i < manifest.chunks.length; i++) {
|
|
4086
|
+
const chunk = manifest.chunks[i];
|
|
4087
|
+
this.reportProgress(
|
|
4088
|
+
"documents",
|
|
4089
|
+
allDocuments.length,
|
|
4090
|
+
manifest.documentCount,
|
|
4091
|
+
`Loading chunk ${chunk.id}...`
|
|
4092
|
+
);
|
|
4093
|
+
try {
|
|
4094
|
+
const documents = await this.loadChunk(staticPath, chunk);
|
|
4095
|
+
for (const doc of documents) {
|
|
4096
|
+
if (!doc._id) {
|
|
4097
|
+
logger.warn(`Document without _id found in chunk ${chunk.id}, skipping`);
|
|
4098
|
+
continue;
|
|
4099
|
+
}
|
|
4100
|
+
if (documentMap.has(doc._id)) {
|
|
4101
|
+
logger.warn(`Duplicate document ID found: ${doc._id}, using latest version`);
|
|
4102
|
+
}
|
|
4103
|
+
documentMap.set(doc._id, doc);
|
|
4104
|
+
}
|
|
4105
|
+
} catch (error) {
|
|
4106
|
+
throw new Error(
|
|
4107
|
+
`Failed to load chunk ${chunk.id}: ${error instanceof Error ? error.message : String(error)}`
|
|
4108
|
+
);
|
|
4109
|
+
}
|
|
4110
|
+
}
|
|
4111
|
+
allDocuments.push(...documentMap.values());
|
|
4112
|
+
logger.info(
|
|
4113
|
+
`Aggregated ${allDocuments.length} unique documents from ${manifest.chunks.length} chunks`
|
|
4114
|
+
);
|
|
4115
|
+
return allDocuments;
|
|
4116
|
+
}
|
|
4117
|
+
/**
|
|
4118
|
+
* Load documents from a single chunk file
|
|
4119
|
+
*/
|
|
4120
|
+
async loadChunk(staticPath, chunk) {
|
|
4121
|
+
try {
|
|
4122
|
+
let chunkContent;
|
|
4123
|
+
let chunkPath;
|
|
4124
|
+
if (this.fs) {
|
|
4125
|
+
chunkPath = this.fs.joinPath(staticPath, chunk.path);
|
|
4126
|
+
chunkContent = await this.fs.readFile(chunkPath);
|
|
4127
|
+
} else {
|
|
4128
|
+
chunkPath = nodeFS2 && nodePath ? nodePath.join(staticPath, chunk.path) : `${staticPath}/${chunk.path}`;
|
|
4129
|
+
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
4130
|
+
chunkContent = await nodeFS2.promises.readFile(chunkPath, "utf8");
|
|
4131
|
+
} else {
|
|
4132
|
+
const response = await fetch(chunkPath);
|
|
4133
|
+
if (!response.ok) {
|
|
4134
|
+
throw new Error(`Failed to fetch chunk: ${response.status} ${response.statusText}`);
|
|
4135
|
+
}
|
|
4136
|
+
chunkContent = await response.text();
|
|
4137
|
+
}
|
|
4138
|
+
}
|
|
4139
|
+
const documents = JSON.parse(chunkContent);
|
|
4140
|
+
if (!Array.isArray(documents)) {
|
|
4141
|
+
throw new Error("Chunk file does not contain an array of documents");
|
|
4142
|
+
}
|
|
4143
|
+
return documents;
|
|
4144
|
+
} catch (error) {
|
|
4145
|
+
const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load chunk: ${error instanceof Error ? error.message : String(error)}`;
|
|
4146
|
+
throw new Error(errorMessage);
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
4149
|
+
/**
|
|
4150
|
+
* Upload documents to CouchDB in batches
|
|
4151
|
+
*/
|
|
4152
|
+
async uploadDocuments(documents, db) {
|
|
4153
|
+
const result = { restored: 0, errors: [], warnings: [] };
|
|
4154
|
+
const batchSize = this.options.chunkBatchSize;
|
|
4155
|
+
for (let i = 0; i < documents.length; i += batchSize) {
|
|
4156
|
+
const batch = documents.slice(i, i + batchSize);
|
|
4157
|
+
this.reportProgress(
|
|
4158
|
+
"documents",
|
|
4159
|
+
i,
|
|
4160
|
+
documents.length,
|
|
4161
|
+
`Uploading batch ${Math.floor(i / batchSize) + 1}...`
|
|
4162
|
+
);
|
|
4163
|
+
try {
|
|
4164
|
+
const docsToInsert = batch.map((doc) => {
|
|
4165
|
+
const cleanDoc = { ...doc };
|
|
4166
|
+
delete cleanDoc._rev;
|
|
4167
|
+
delete cleanDoc._attachments;
|
|
4168
|
+
return cleanDoc;
|
|
4169
|
+
});
|
|
4170
|
+
const bulkResult = await db.bulkDocs(docsToInsert);
|
|
4171
|
+
for (let j = 0; j < bulkResult.length; j++) {
|
|
4172
|
+
const docResult = bulkResult[j];
|
|
4173
|
+
const originalDoc = batch[j];
|
|
4174
|
+
if ("error" in docResult) {
|
|
4175
|
+
const errorMessage = `Failed to upload document ${originalDoc._id}: ${docResult.error} - ${docResult.reason}`;
|
|
4176
|
+
result.errors.push(errorMessage);
|
|
4177
|
+
logger.error(errorMessage);
|
|
4178
|
+
} else {
|
|
4179
|
+
result.restored++;
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
} catch (error) {
|
|
4183
|
+
let errorMessage;
|
|
4184
|
+
if (error instanceof Error) {
|
|
4185
|
+
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
4186
|
+
} else if (error && typeof error === "object" && "message" in error) {
|
|
4187
|
+
errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
|
|
4188
|
+
} else {
|
|
4189
|
+
errorMessage = `Failed to upload document batch starting at index ${i}: ${JSON.stringify(error)}`;
|
|
4190
|
+
}
|
|
4191
|
+
result.errors.push(errorMessage);
|
|
4192
|
+
logger.error(errorMessage);
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
this.reportProgress(
|
|
4196
|
+
"documents",
|
|
4197
|
+
documents.length,
|
|
4198
|
+
documents.length,
|
|
4199
|
+
`Uploaded ${result.restored} documents`
|
|
4200
|
+
);
|
|
4201
|
+
return result;
|
|
4202
|
+
}
|
|
4203
|
+
/**
|
|
4204
|
+
* Upload attachments from filesystem to CouchDB
|
|
4205
|
+
*/
|
|
4206
|
+
async uploadAttachments(staticPath, documents, db) {
|
|
4207
|
+
const result = { restored: 0, errors: [], warnings: [] };
|
|
4208
|
+
let processedDocs = 0;
|
|
4209
|
+
for (const doc of documents) {
|
|
4210
|
+
this.reportProgress(
|
|
4211
|
+
"attachments",
|
|
4212
|
+
processedDocs,
|
|
4213
|
+
documents.length,
|
|
4214
|
+
`Processing attachments for ${doc._id}...`
|
|
4215
|
+
);
|
|
4216
|
+
processedDocs++;
|
|
4217
|
+
if (!doc._attachments) {
|
|
4218
|
+
continue;
|
|
4219
|
+
}
|
|
4220
|
+
for (const [attachmentName, attachmentMeta] of Object.entries(doc._attachments)) {
|
|
4221
|
+
try {
|
|
4222
|
+
const uploadResult = await this.uploadSingleAttachment(
|
|
4223
|
+
staticPath,
|
|
4224
|
+
doc._id,
|
|
4225
|
+
attachmentName,
|
|
4226
|
+
attachmentMeta,
|
|
4227
|
+
db
|
|
4228
|
+
);
|
|
4229
|
+
if (uploadResult.success) {
|
|
4230
|
+
result.restored++;
|
|
4231
|
+
} else {
|
|
4232
|
+
result.errors.push(uploadResult.error || "Unknown attachment upload error");
|
|
4233
|
+
}
|
|
4234
|
+
} catch (error) {
|
|
4235
|
+
const errorMessage = `Failed to upload attachment ${doc._id}/${attachmentName}: ${error instanceof Error ? error.message : String(error)}`;
|
|
4236
|
+
result.errors.push(errorMessage);
|
|
4237
|
+
logger.error(errorMessage);
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
this.reportProgress(
|
|
4242
|
+
"attachments",
|
|
4243
|
+
documents.length,
|
|
4244
|
+
documents.length,
|
|
4245
|
+
`Uploaded ${result.restored} attachments`
|
|
4246
|
+
);
|
|
4247
|
+
return result;
|
|
4248
|
+
}
|
|
4249
|
+
/**
|
|
4250
|
+
* Upload a single attachment file
|
|
4251
|
+
*/
|
|
4252
|
+
async uploadSingleAttachment(staticPath, docId, attachmentName, attachmentMeta, db) {
|
|
4253
|
+
const result = {
|
|
4254
|
+
success: false,
|
|
4255
|
+
attachmentName,
|
|
4256
|
+
docId
|
|
4257
|
+
};
|
|
4258
|
+
try {
|
|
4259
|
+
if (!attachmentMeta.path) {
|
|
4260
|
+
result.error = "Attachment metadata missing file path";
|
|
4261
|
+
return result;
|
|
4262
|
+
}
|
|
4263
|
+
let attachmentData;
|
|
4264
|
+
let attachmentPath;
|
|
4265
|
+
if (this.fs) {
|
|
4266
|
+
attachmentPath = this.fs.joinPath(staticPath, attachmentMeta.path);
|
|
4267
|
+
attachmentData = await this.fs.readBinary(attachmentPath);
|
|
4268
|
+
} else {
|
|
4269
|
+
attachmentPath = nodeFS2 && nodePath ? nodePath.join(staticPath, attachmentMeta.path) : `${staticPath}/${attachmentMeta.path}`;
|
|
4270
|
+
if (nodeFS2 && this.isLocalPath(staticPath)) {
|
|
4271
|
+
attachmentData = await nodeFS2.promises.readFile(attachmentPath);
|
|
4272
|
+
} else {
|
|
4273
|
+
const response = await fetch(attachmentPath);
|
|
4274
|
+
if (!response.ok) {
|
|
4275
|
+
result.error = `Failed to fetch attachment: ${response.status} ${response.statusText}`;
|
|
4276
|
+
return result;
|
|
4277
|
+
}
|
|
4278
|
+
attachmentData = await response.arrayBuffer();
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
const doc = await db.get(docId);
|
|
4282
|
+
await db.putAttachment(
|
|
4283
|
+
docId,
|
|
4284
|
+
attachmentName,
|
|
4285
|
+
doc._rev,
|
|
4286
|
+
attachmentData,
|
|
4287
|
+
// PouchDB accepts both ArrayBuffer and Buffer
|
|
4288
|
+
attachmentMeta.content_type
|
|
4289
|
+
);
|
|
4290
|
+
result.success = true;
|
|
4291
|
+
} catch (error) {
|
|
4292
|
+
result.error = error instanceof Error ? error.message : String(error);
|
|
4293
|
+
}
|
|
4294
|
+
return result;
|
|
4295
|
+
}
|
|
4296
|
+
/**
|
|
4297
|
+
* Restore CourseConfig document from manifest
|
|
4298
|
+
*/
|
|
4299
|
+
async restoreCourseConfig(manifest, targetDB) {
|
|
4300
|
+
const results = {
|
|
4301
|
+
restored: 0,
|
|
4302
|
+
errors: [],
|
|
4303
|
+
warnings: []
|
|
4304
|
+
};
|
|
4305
|
+
try {
|
|
4306
|
+
if (!manifest.courseConfig) {
|
|
4307
|
+
results.warnings.push(
|
|
4308
|
+
"No courseConfig found in manifest, skipping CourseConfig document creation"
|
|
4309
|
+
);
|
|
4310
|
+
return results;
|
|
4311
|
+
}
|
|
4312
|
+
const courseConfigDoc = {
|
|
4313
|
+
_id: "CourseConfig",
|
|
4314
|
+
...manifest.courseConfig,
|
|
4315
|
+
courseID: manifest.courseId
|
|
4316
|
+
};
|
|
4317
|
+
delete courseConfigDoc._rev;
|
|
4318
|
+
await targetDB.put(courseConfigDoc);
|
|
4319
|
+
results.restored = 1;
|
|
4320
|
+
logger.info(`CourseConfig document created for course: ${manifest.courseId}`);
|
|
4321
|
+
} catch (error) {
|
|
4322
|
+
const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
|
|
4323
|
+
results.errors.push(`Failed to restore CourseConfig: ${errorMessage}`);
|
|
4324
|
+
logger.error("CourseConfig restoration failed:", error);
|
|
4325
|
+
}
|
|
4326
|
+
return results;
|
|
4327
|
+
}
|
|
4328
|
+
/**
|
|
4329
|
+
* Calculate expected document counts from manifest
|
|
4330
|
+
*/
|
|
4331
|
+
calculateExpectedCounts(manifest) {
|
|
4332
|
+
const counts = {};
|
|
4333
|
+
for (const chunk of manifest.chunks) {
|
|
4334
|
+
counts[chunk.docType] = (counts[chunk.docType] || 0) + chunk.documentCount;
|
|
4335
|
+
}
|
|
4336
|
+
if (manifest.designDocs.length > 0) {
|
|
4337
|
+
counts["_design"] = manifest.designDocs.length;
|
|
4338
|
+
}
|
|
4339
|
+
return counts;
|
|
4340
|
+
}
|
|
4341
|
+
/**
|
|
4342
|
+
* Clean up database after failed migration
|
|
4343
|
+
*/
|
|
4344
|
+
async cleanupFailedMigration(db) {
|
|
4345
|
+
logger.info("Cleaning up failed migration...");
|
|
4346
|
+
try {
|
|
4347
|
+
const allDocs = await db.allDocs();
|
|
4348
|
+
const docsToDelete = allDocs.rows.map((row) => ({
|
|
4349
|
+
_id: row.id,
|
|
4350
|
+
_rev: row.value.rev,
|
|
4351
|
+
_deleted: true
|
|
4352
|
+
}));
|
|
4353
|
+
if (docsToDelete.length > 0) {
|
|
4354
|
+
await db.bulkDocs(docsToDelete);
|
|
4355
|
+
logger.info(`Cleaned up ${docsToDelete.length} documents from failed migration`);
|
|
4356
|
+
}
|
|
4357
|
+
} catch (error) {
|
|
4358
|
+
logger.error("Failed to cleanup documents:", error);
|
|
4359
|
+
throw error;
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
/**
|
|
4363
|
+
* Report progress to callback if available
|
|
4364
|
+
*/
|
|
4365
|
+
reportProgress(phase, current, total, message) {
|
|
4366
|
+
if (this.progressCallback) {
|
|
4367
|
+
this.progressCallback({
|
|
4368
|
+
phase,
|
|
4369
|
+
current,
|
|
4370
|
+
total,
|
|
4371
|
+
message
|
|
4372
|
+
});
|
|
4373
|
+
}
|
|
4374
|
+
}
|
|
4375
|
+
/**
|
|
4376
|
+
* Check if a path is a local file path (vs URL)
|
|
4377
|
+
*/
|
|
4378
|
+
isLocalPath(path2) {
|
|
4379
|
+
return !path2.startsWith("http://") && !path2.startsWith("https://");
|
|
4380
|
+
}
|
|
4381
|
+
};
|
|
4382
|
+
}
|
|
4383
|
+
});
|
|
4384
|
+
|
|
4385
|
+
// src/util/migrator/index.ts
|
|
4386
|
+
var init_migrator = __esm({
|
|
4387
|
+
"src/util/migrator/index.ts"() {
|
|
4388
|
+
"use strict";
|
|
4389
|
+
init_StaticToCouchDBMigrator();
|
|
4390
|
+
init_validation();
|
|
4391
|
+
init_FileSystemAdapter();
|
|
4392
|
+
}
|
|
4393
|
+
});
|
|
4394
|
+
|
|
4395
|
+
// src/util/index.ts
|
|
4396
|
+
var init_util2 = __esm({
|
|
4397
|
+
"src/util/index.ts"() {
|
|
4398
|
+
"use strict";
|
|
4399
|
+
init_Loggable();
|
|
4400
|
+
init_packer();
|
|
4401
|
+
init_migrator();
|
|
4402
|
+
init_dataDirectory();
|
|
4403
|
+
}
|
|
4404
|
+
});
|
|
4405
|
+
|
|
4406
|
+
// src/study/SourceMixer.ts
|
|
4407
|
+
var init_SourceMixer = __esm({
|
|
4408
|
+
"src/study/SourceMixer.ts"() {
|
|
4409
|
+
"use strict";
|
|
4410
|
+
}
|
|
4411
|
+
});
|
|
4412
|
+
|
|
4413
|
+
// src/study/MixerDebugger.ts
|
|
4414
|
+
function printMixerSummary(run) {
|
|
4415
|
+
console.group(`\u{1F3A8} Mixer Run: ${run.mixerType}`);
|
|
4416
|
+
logger.info(`Run ID: ${run.runId}`);
|
|
4417
|
+
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
4418
|
+
logger.info(
|
|
4419
|
+
`Config: limit=${run.requestedLimit}${run.quotaPerSource ? `, quota/source=${run.quotaPerSource}` : ""}`
|
|
4420
|
+
);
|
|
4421
|
+
console.group(`\u{1F4E5} Input: ${run.sourceSummaries.length} sources`);
|
|
4422
|
+
for (const src of run.sourceSummaries) {
|
|
4423
|
+
logger.info(
|
|
4424
|
+
` ${src.sourceName || src.sourceId}: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`
|
|
4425
|
+
);
|
|
4426
|
+
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}], avg: ${src.avgScore.toFixed(2)}`);
|
|
4427
|
+
}
|
|
4428
|
+
console.groupEnd();
|
|
4429
|
+
console.group(`\u{1F4E4} Output: ${run.finalCount} cards selected (${run.reviewsSelected} reviews, ${run.newSelected} new)`);
|
|
4430
|
+
for (const breakdown of run.sourceBreakdowns) {
|
|
4431
|
+
const name = breakdown.sourceName || breakdown.sourceId;
|
|
4432
|
+
logger.info(
|
|
4433
|
+
` ${name}: ${breakdown.totalSelected} selected (${breakdown.reviewsSelected} reviews, ${breakdown.newSelected} new) - ${breakdown.selectionRate.toFixed(1)}% selection rate`
|
|
4434
|
+
);
|
|
4435
|
+
}
|
|
4436
|
+
console.groupEnd();
|
|
4437
|
+
console.groupEnd();
|
|
4438
|
+
}
|
|
4439
|
+
function mountMixerDebugger() {
|
|
4440
|
+
if (typeof window === "undefined") return;
|
|
4441
|
+
const win = window;
|
|
4442
|
+
win.skuilder = win.skuilder || {};
|
|
4443
|
+
win.skuilder.mixer = mixerDebugAPI;
|
|
4444
|
+
}
|
|
4445
|
+
var runHistory2, mixerDebugAPI;
|
|
4446
|
+
var init_MixerDebugger = __esm({
|
|
4447
|
+
"src/study/MixerDebugger.ts"() {
|
|
4448
|
+
"use strict";
|
|
4449
|
+
init_logger();
|
|
4450
|
+
init_navigators();
|
|
4451
|
+
runHistory2 = [];
|
|
4452
|
+
mixerDebugAPI = {
|
|
4453
|
+
/**
|
|
4454
|
+
* Get raw run history for programmatic access.
|
|
4455
|
+
*/
|
|
4456
|
+
get runs() {
|
|
4457
|
+
return [...runHistory2];
|
|
4458
|
+
},
|
|
4459
|
+
/**
|
|
4460
|
+
* Show summary of a specific mixer run.
|
|
4461
|
+
*/
|
|
4462
|
+
showRun(idOrIndex = 0) {
|
|
4463
|
+
if (runHistory2.length === 0) {
|
|
4464
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4465
|
+
return;
|
|
4466
|
+
}
|
|
4467
|
+
let run;
|
|
4468
|
+
if (typeof idOrIndex === "number") {
|
|
4469
|
+
run = runHistory2[idOrIndex];
|
|
4470
|
+
if (!run) {
|
|
4471
|
+
logger.info(`[Mixer Debug] No run found at index ${idOrIndex}. History length: ${runHistory2.length}`);
|
|
4472
|
+
return;
|
|
4473
|
+
}
|
|
4474
|
+
} else {
|
|
4475
|
+
run = runHistory2.find((r) => r.runId.endsWith(idOrIndex));
|
|
4476
|
+
if (!run) {
|
|
4477
|
+
logger.info(`[Mixer Debug] No run found matching ID '${idOrIndex}'.`);
|
|
4478
|
+
return;
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4481
|
+
printMixerSummary(run);
|
|
4482
|
+
},
|
|
4483
|
+
/**
|
|
4484
|
+
* Show summary of the last mixer run.
|
|
4485
|
+
*/
|
|
4486
|
+
showLastMix() {
|
|
4487
|
+
this.showRun(0);
|
|
4488
|
+
},
|
|
4489
|
+
/**
|
|
4490
|
+
* Explain source balance in the last run.
|
|
4491
|
+
*/
|
|
4492
|
+
explainSourceBalance() {
|
|
4493
|
+
if (runHistory2.length === 0) {
|
|
4494
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4495
|
+
return;
|
|
4496
|
+
}
|
|
4497
|
+
const run = runHistory2[0];
|
|
4498
|
+
console.group("\u2696\uFE0F Source Balance Analysis");
|
|
4499
|
+
logger.info(`Mixer: ${run.mixerType}`);
|
|
4500
|
+
logger.info(`Requested limit: ${run.requestedLimit}`);
|
|
4501
|
+
if (run.quotaPerSource) {
|
|
4502
|
+
logger.info(`Quota per source: ${run.quotaPerSource}`);
|
|
4503
|
+
}
|
|
4504
|
+
console.group("Input Distribution:");
|
|
4505
|
+
for (const src of run.sourceSummaries) {
|
|
4506
|
+
const name = src.sourceName || src.sourceId;
|
|
4507
|
+
logger.info(`${name}:`);
|
|
4508
|
+
logger.info(` Provided: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`);
|
|
4509
|
+
logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}]`);
|
|
4510
|
+
}
|
|
4511
|
+
console.groupEnd();
|
|
4512
|
+
console.group("Selection Results:");
|
|
4513
|
+
for (const breakdown of run.sourceBreakdowns) {
|
|
4514
|
+
const name = breakdown.sourceName || breakdown.sourceId;
|
|
4515
|
+
logger.info(`${name}:`);
|
|
4516
|
+
logger.info(
|
|
4517
|
+
` Selected: ${breakdown.totalSelected}/${breakdown.reviewsProvided + breakdown.newProvided} (${breakdown.selectionRate.toFixed(1)}%)`
|
|
4518
|
+
);
|
|
4519
|
+
logger.info(` Reviews: ${breakdown.reviewsSelected}/${breakdown.reviewsProvided}`);
|
|
4520
|
+
logger.info(` New: ${breakdown.newSelected}/${breakdown.newProvided}`);
|
|
4521
|
+
if (breakdown.reviewsProvided > 0 && breakdown.reviewsSelected === 0) {
|
|
4522
|
+
logger.info(` \u26A0\uFE0F Had reviews but none selected!`);
|
|
4523
|
+
}
|
|
4524
|
+
if (breakdown.totalSelected === 0 && breakdown.reviewsProvided + breakdown.newProvided > 0) {
|
|
4525
|
+
logger.info(` \u26A0\uFE0F Had cards but none selected!`);
|
|
4526
|
+
}
|
|
4527
|
+
}
|
|
4528
|
+
console.groupEnd();
|
|
4529
|
+
const selectionRates = run.sourceBreakdowns.map((b) => b.selectionRate);
|
|
4530
|
+
const avgRate = selectionRates.reduce((a, b) => a + b, 0) / selectionRates.length;
|
|
4531
|
+
const maxDeviation = Math.max(...selectionRates.map((r) => Math.abs(r - avgRate)));
|
|
4532
|
+
if (maxDeviation > 20) {
|
|
4533
|
+
logger.info(`
|
|
4534
|
+
\u26A0\uFE0F Significant imbalance detected (max deviation: ${maxDeviation.toFixed(1)}%)`);
|
|
4535
|
+
logger.info("Possible causes:");
|
|
4536
|
+
logger.info(" - Score range differences between sources");
|
|
4537
|
+
logger.info(" - One source has much better quality cards");
|
|
4538
|
+
logger.info(" - Different card availability (reviews vs new)");
|
|
4539
|
+
}
|
|
4540
|
+
console.groupEnd();
|
|
4541
|
+
},
|
|
4542
|
+
/**
|
|
4543
|
+
* Compare score distributions across sources.
|
|
4544
|
+
*/
|
|
4545
|
+
compareScores() {
|
|
4546
|
+
if (runHistory2.length === 0) {
|
|
4547
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4548
|
+
return;
|
|
4549
|
+
}
|
|
4550
|
+
const run = runHistory2[0];
|
|
4551
|
+
console.group("\u{1F4CA} Score Distribution Comparison");
|
|
4552
|
+
console.table(
|
|
4553
|
+
run.sourceSummaries.map((src) => ({
|
|
4554
|
+
source: src.sourceName || src.sourceId,
|
|
4555
|
+
cards: src.totalCards,
|
|
4556
|
+
min: src.bottomScore.toFixed(3),
|
|
4557
|
+
max: src.topScore.toFixed(3),
|
|
4558
|
+
avg: src.avgScore.toFixed(3),
|
|
4559
|
+
range: (src.topScore - src.bottomScore).toFixed(3)
|
|
4560
|
+
}))
|
|
4561
|
+
);
|
|
4562
|
+
const ranges = run.sourceSummaries.map((s) => s.topScore - s.bottomScore);
|
|
4563
|
+
const avgScores = run.sourceSummaries.map((s) => s.avgScore);
|
|
4564
|
+
const rangeDiff = Math.max(...ranges) - Math.min(...ranges);
|
|
4565
|
+
const avgDiff = Math.max(...avgScores) - Math.min(...avgScores);
|
|
4566
|
+
if (rangeDiff > 0.3 || avgDiff > 0.2) {
|
|
4567
|
+
logger.info("\n\u26A0\uFE0F Significant score distribution differences detected");
|
|
4568
|
+
logger.info(
|
|
4569
|
+
"This may cause one source to dominate selection if using global sorting (not quota-based)"
|
|
4570
|
+
);
|
|
4571
|
+
}
|
|
4572
|
+
console.groupEnd();
|
|
4573
|
+
},
|
|
4574
|
+
/**
|
|
4575
|
+
* Show detailed information for a specific card.
|
|
4576
|
+
*/
|
|
4577
|
+
showCard(cardId) {
|
|
4578
|
+
for (const run of runHistory2) {
|
|
4579
|
+
const card = run.cards.find((c) => c.cardId === cardId);
|
|
4580
|
+
if (card) {
|
|
4581
|
+
const source = run.sourceSummaries.find((s) => s.sourceIndex === card.sourceIndex);
|
|
4582
|
+
console.group(`\u{1F3B4} Card: ${cardId}`);
|
|
4583
|
+
logger.info(`Course: ${card.courseId}`);
|
|
4584
|
+
logger.info(`Source: ${source?.sourceName || source?.sourceId || "unknown"}`);
|
|
4585
|
+
logger.info(`Origin: ${card.origin}`);
|
|
4586
|
+
logger.info(`Score: ${card.score.toFixed(3)}`);
|
|
4587
|
+
if (card.rankInSource) {
|
|
4588
|
+
logger.info(`Rank in source: #${card.rankInSource}`);
|
|
4589
|
+
}
|
|
4590
|
+
if (card.rankInMix) {
|
|
4591
|
+
logger.info(`Rank in mixed results: #${card.rankInMix}`);
|
|
4592
|
+
}
|
|
4593
|
+
logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
|
|
4594
|
+
if (!card.selected && card.rankInSource) {
|
|
4595
|
+
logger.info("\nWhy not selected:");
|
|
4596
|
+
if (run.quotaPerSource && card.rankInSource > run.quotaPerSource) {
|
|
4597
|
+
logger.info(` - Ranked #${card.rankInSource} in source, but quota was ${run.quotaPerSource}`);
|
|
4598
|
+
}
|
|
4599
|
+
logger.info(" - Check score compared to selected cards using .showRun()");
|
|
4600
|
+
}
|
|
4601
|
+
console.groupEnd();
|
|
4602
|
+
return;
|
|
4603
|
+
}
|
|
4604
|
+
}
|
|
4605
|
+
logger.info(`[Mixer Debug] Card '${cardId}' not found in recent runs.`);
|
|
4606
|
+
},
|
|
4607
|
+
/**
|
|
4608
|
+
* Show all runs in compact format.
|
|
4609
|
+
*/
|
|
4610
|
+
listRuns() {
|
|
4611
|
+
if (runHistory2.length === 0) {
|
|
4612
|
+
logger.info("[Mixer Debug] No runs captured yet.");
|
|
4613
|
+
return;
|
|
4614
|
+
}
|
|
4615
|
+
console.table(
|
|
4616
|
+
runHistory2.map((r) => ({
|
|
4617
|
+
id: r.runId.slice(-8),
|
|
4618
|
+
time: r.timestamp.toLocaleTimeString(),
|
|
4619
|
+
mixer: r.mixerType,
|
|
4620
|
+
sources: r.sourceSummaries.length,
|
|
4621
|
+
selected: r.finalCount,
|
|
4622
|
+
reviews: r.reviewsSelected,
|
|
4623
|
+
new: r.newSelected
|
|
4624
|
+
}))
|
|
4625
|
+
);
|
|
4626
|
+
},
|
|
4627
|
+
/**
|
|
4628
|
+
* Export run history as JSON for bug reports.
|
|
4629
|
+
*/
|
|
4630
|
+
export() {
|
|
4631
|
+
const json = JSON.stringify(runHistory2, null, 2);
|
|
4632
|
+
logger.info("[Mixer Debug] Run history exported. Copy the returned string or use:");
|
|
4633
|
+
logger.info(" copy(window.skuilder.mixer.export())");
|
|
4634
|
+
return json;
|
|
4635
|
+
},
|
|
4636
|
+
/**
|
|
4637
|
+
* Clear run history.
|
|
4638
|
+
*/
|
|
4639
|
+
clear() {
|
|
4640
|
+
runHistory2.length = 0;
|
|
4641
|
+
logger.info("[Mixer Debug] Run history cleared.");
|
|
4642
|
+
},
|
|
4643
|
+
/**
|
|
4644
|
+
* Show help.
|
|
4645
|
+
*/
|
|
4646
|
+
help() {
|
|
4647
|
+
logger.info(`
|
|
4648
|
+
\u{1F3A8} Mixer Debug API
|
|
4649
|
+
|
|
4650
|
+
Commands:
|
|
4651
|
+
.showLastMix() Show summary of most recent mixer run
|
|
4652
|
+
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
4653
|
+
.explainSourceBalance() Analyze source balance and selection patterns
|
|
4654
|
+
.compareScores() Compare score distributions across sources
|
|
4655
|
+
.showCard(cardId) Show mixer decisions for a specific card
|
|
4656
|
+
.listRuns() List all captured runs in table format
|
|
4657
|
+
.export() Export run history as JSON for bug reports
|
|
4658
|
+
.clear() Clear run history
|
|
4659
|
+
.runs Access raw run history array
|
|
4660
|
+
.help() Show this help message
|
|
4661
|
+
|
|
4662
|
+
Example:
|
|
4663
|
+
window.skuilder.mixer.showLastMix()
|
|
4664
|
+
window.skuilder.mixer.explainSourceBalance()
|
|
4665
|
+
window.skuilder.mixer.compareScores()
|
|
4666
|
+
`);
|
|
4667
|
+
}
|
|
4668
|
+
};
|
|
4669
|
+
mountMixerDebugger();
|
|
4670
|
+
}
|
|
4671
|
+
});
|
|
4672
|
+
|
|
4673
|
+
// src/study/SessionDebugger.ts
|
|
4674
|
+
function showCurrentQueue() {
|
|
4675
|
+
if (!activeSession) {
|
|
4676
|
+
logger.info("[Session Debug] No active session.");
|
|
4677
|
+
return;
|
|
2742
4678
|
}
|
|
2743
|
-
const
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
4679
|
+
const latest = activeSession.queueSnapshots[activeSession.queueSnapshots.length - 1] || activeSession.initialQueues;
|
|
4680
|
+
console.group("\u{1F4CA} Current Queue State");
|
|
4681
|
+
logger.info(`Review Queue: ${latest.reviewQLength} cards`);
|
|
4682
|
+
if (latest.reviewQNext3 && latest.reviewQNext3.length > 0) {
|
|
4683
|
+
logger.info(` Next: ${latest.reviewQNext3.join(", ")}`);
|
|
2748
4684
|
}
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
}
|
|
2752
|
-
function scoreAccuracyInZone(accuracy, target, tolerance) {
|
|
2753
|
-
const dist = Math.abs(accuracy - target);
|
|
2754
|
-
if (dist <= tolerance) {
|
|
2755
|
-
return 1;
|
|
4685
|
+
logger.info(`New Queue: ${latest.newQLength} cards`);
|
|
4686
|
+
if (latest.newQNext3 && latest.newQNext3.length > 0) {
|
|
4687
|
+
logger.info(` Next: ${latest.newQNext3.join(", ")}`);
|
|
2756
4688
|
}
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
return Math.max(0, 1 - excess * slope);
|
|
4689
|
+
logger.info(`Failed Queue: ${latest.failedQLength} cards`);
|
|
4690
|
+
console.groupEnd();
|
|
2760
4691
|
}
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
4692
|
+
function showPresentationHistory(sessionIndex = 0) {
|
|
4693
|
+
const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
|
|
4694
|
+
if (!session) {
|
|
4695
|
+
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
4696
|
+
return;
|
|
2764
4697
|
}
|
|
2765
|
-
});
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
4698
|
+
console.group(`\u{1F4DC} Session History: ${session.sessionId}`);
|
|
4699
|
+
logger.info(`Started: ${session.startTime.toLocaleTimeString()}`);
|
|
4700
|
+
if (session.endTime) {
|
|
4701
|
+
logger.info(`Ended: ${session.endTime.toLocaleTimeString()}`);
|
|
4702
|
+
}
|
|
4703
|
+
logger.info(`Cards presented: ${session.presentations.length}`);
|
|
4704
|
+
if (session.presentations.length > 0) {
|
|
4705
|
+
console.table(
|
|
4706
|
+
session.presentations.map((p) => ({
|
|
4707
|
+
"#": p.sequenceNumber,
|
|
4708
|
+
course: p.courseName || p.courseId.slice(0, 8),
|
|
4709
|
+
origin: p.origin,
|
|
4710
|
+
queue: p.queueSource,
|
|
4711
|
+
score: p.score?.toFixed(3) || "-",
|
|
4712
|
+
time: p.timestamp.toLocaleTimeString()
|
|
4713
|
+
}))
|
|
2775
4714
|
);
|
|
4715
|
+
}
|
|
4716
|
+
console.groupEnd();
|
|
4717
|
+
}
|
|
4718
|
+
function showInterleaving(sessionIndex = 0) {
|
|
4719
|
+
const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
|
|
4720
|
+
if (!session) {
|
|
4721
|
+
logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
|
|
2776
4722
|
return;
|
|
2777
4723
|
}
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
4724
|
+
console.group("\u{1F500} Interleaving Analysis");
|
|
4725
|
+
const courseCounts = /* @__PURE__ */ new Map();
|
|
4726
|
+
const courseOrigins = /* @__PURE__ */ new Map();
|
|
4727
|
+
session.presentations.forEach((p) => {
|
|
4728
|
+
const name = p.courseName || p.courseId;
|
|
4729
|
+
courseCounts.set(name, (courseCounts.get(name) || 0) + 1);
|
|
4730
|
+
if (!courseOrigins.has(name)) {
|
|
4731
|
+
courseOrigins.set(name, { review: 0, new: 0, failed: 0 });
|
|
4732
|
+
}
|
|
4733
|
+
const origins = courseOrigins.get(name);
|
|
4734
|
+
origins[p.origin]++;
|
|
4735
|
+
});
|
|
4736
|
+
logger.info("Course distribution:");
|
|
4737
|
+
console.table(
|
|
4738
|
+
Array.from(courseCounts.entries()).map(([course, count]) => {
|
|
4739
|
+
const origins = courseOrigins.get(course);
|
|
4740
|
+
return {
|
|
4741
|
+
course,
|
|
4742
|
+
total: count,
|
|
4743
|
+
reviews: origins.review,
|
|
4744
|
+
new: origins.new,
|
|
4745
|
+
failed: origins.failed,
|
|
4746
|
+
percentage: (count / session.presentations.length * 100).toFixed(1) + "%"
|
|
4747
|
+
};
|
|
4748
|
+
})
|
|
4749
|
+
);
|
|
4750
|
+
if (session.presentations.length > 0) {
|
|
4751
|
+
logger.info("\nPresentation sequence (first 20):");
|
|
4752
|
+
const sequence = session.presentations.slice(0, 20).map((p, idx) => `${idx + 1}. ${p.courseName || p.courseId.slice(0, 8)} (${p.origin})`).join("\n");
|
|
4753
|
+
logger.info(sequence);
|
|
2781
4754
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
courseId
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
metadata: {
|
|
2793
|
-
sessionsCount: 1,
|
|
2794
|
-
// Assumes recording is triggered per-session currently
|
|
2795
|
-
cardsSeen: records.length,
|
|
2796
|
-
eloStart,
|
|
2797
|
-
eloEnd,
|
|
2798
|
-
signalType: "accuracy_in_zone"
|
|
4755
|
+
let maxCluster = 0;
|
|
4756
|
+
let currentCluster = 1;
|
|
4757
|
+
let currentCourse = session.presentations[0]?.courseId;
|
|
4758
|
+
for (let i = 1; i < session.presentations.length; i++) {
|
|
4759
|
+
if (session.presentations[i].courseId === currentCourse) {
|
|
4760
|
+
currentCluster++;
|
|
4761
|
+
maxCluster = Math.max(maxCluster, currentCluster);
|
|
4762
|
+
} else {
|
|
4763
|
+
currentCourse = session.presentations[i].courseId;
|
|
4764
|
+
currentCluster = 1;
|
|
2799
4765
|
}
|
|
2800
|
-
};
|
|
2801
|
-
try {
|
|
2802
|
-
await user.putUserOutcome(record);
|
|
2803
|
-
logger.debug(
|
|
2804
|
-
`[Orchestration] Recorded outcome ${outcomeValue.toFixed(3)} for ${userId} (doc: ${id})`
|
|
2805
|
-
);
|
|
2806
|
-
} catch (e) {
|
|
2807
|
-
logger.error(`[Orchestration] Failed to record outcome: ${e}`);
|
|
2808
4766
|
}
|
|
4767
|
+
if (maxCluster > 3) {
|
|
4768
|
+
logger.info(`
|
|
4769
|
+
\u26A0\uFE0F Detected clustering: max ${maxCluster} cards from same course in a row`);
|
|
4770
|
+
logger.info("This suggests cards are sorted by score rather than round-robin by course.");
|
|
4771
|
+
}
|
|
4772
|
+
console.groupEnd();
|
|
2809
4773
|
}
|
|
2810
|
-
|
|
2811
|
-
"
|
|
4774
|
+
function mountSessionDebugger() {
|
|
4775
|
+
if (typeof window === "undefined") return;
|
|
4776
|
+
const win = window;
|
|
4777
|
+
win.skuilder = win.skuilder || {};
|
|
4778
|
+
win.skuilder.session = sessionDebugAPI;
|
|
4779
|
+
}
|
|
4780
|
+
var activeSession, sessionHistory, sessionDebugAPI;
|
|
4781
|
+
var init_SessionDebugger = __esm({
|
|
4782
|
+
"src/study/SessionDebugger.ts"() {
|
|
2812
4783
|
"use strict";
|
|
2813
|
-
init_signal();
|
|
2814
|
-
init_types_legacy();
|
|
2815
4784
|
init_logger();
|
|
2816
|
-
|
|
2817
|
-
|
|
4785
|
+
activeSession = null;
|
|
4786
|
+
sessionHistory = [];
|
|
4787
|
+
sessionDebugAPI = {
|
|
4788
|
+
/**
|
|
4789
|
+
* Get raw session history for programmatic access.
|
|
4790
|
+
*/
|
|
4791
|
+
get sessions() {
|
|
4792
|
+
return [...sessionHistory];
|
|
4793
|
+
},
|
|
4794
|
+
/**
|
|
4795
|
+
* Get active session if any.
|
|
4796
|
+
*/
|
|
4797
|
+
get active() {
|
|
4798
|
+
return activeSession;
|
|
4799
|
+
},
|
|
4800
|
+
/**
|
|
4801
|
+
* Show current queue state.
|
|
4802
|
+
*/
|
|
4803
|
+
showQueue() {
|
|
4804
|
+
showCurrentQueue();
|
|
4805
|
+
},
|
|
4806
|
+
/**
|
|
4807
|
+
* Show presentation history for current or past session.
|
|
4808
|
+
*/
|
|
4809
|
+
showHistory(sessionIndex = 0) {
|
|
4810
|
+
showPresentationHistory(sessionIndex);
|
|
4811
|
+
},
|
|
4812
|
+
/**
|
|
4813
|
+
* Analyze course interleaving pattern.
|
|
4814
|
+
*/
|
|
4815
|
+
showInterleaving(sessionIndex = 0) {
|
|
4816
|
+
showInterleaving(sessionIndex);
|
|
4817
|
+
},
|
|
4818
|
+
/**
|
|
4819
|
+
* List all tracked sessions.
|
|
4820
|
+
*/
|
|
4821
|
+
listSessions() {
|
|
4822
|
+
if (activeSession) {
|
|
4823
|
+
logger.info(`Active session: ${activeSession.sessionId} (${activeSession.presentations.length} cards presented)`);
|
|
4824
|
+
}
|
|
4825
|
+
if (sessionHistory.length === 0) {
|
|
4826
|
+
logger.info("[Session Debug] No completed sessions in history.");
|
|
4827
|
+
return;
|
|
4828
|
+
}
|
|
4829
|
+
console.table(
|
|
4830
|
+
sessionHistory.map((s, idx) => ({
|
|
4831
|
+
index: idx,
|
|
4832
|
+
id: s.sessionId.slice(-8),
|
|
4833
|
+
started: s.startTime.toLocaleTimeString(),
|
|
4834
|
+
ended: s.endTime?.toLocaleTimeString() || "incomplete",
|
|
4835
|
+
cards: s.presentations.length
|
|
4836
|
+
}))
|
|
4837
|
+
);
|
|
4838
|
+
},
|
|
4839
|
+
/**
|
|
4840
|
+
* Export session history as JSON for bug reports.
|
|
4841
|
+
*/
|
|
4842
|
+
export() {
|
|
4843
|
+
const data = {
|
|
4844
|
+
active: activeSession,
|
|
4845
|
+
history: sessionHistory
|
|
4846
|
+
};
|
|
4847
|
+
const json = JSON.stringify(data, null, 2);
|
|
4848
|
+
logger.info("[Session Debug] Session data exported. Copy the returned string or use:");
|
|
4849
|
+
logger.info(" copy(window.skuilder.session.export())");
|
|
4850
|
+
return json;
|
|
4851
|
+
},
|
|
4852
|
+
/**
|
|
4853
|
+
* Clear session history.
|
|
4854
|
+
*/
|
|
4855
|
+
clear() {
|
|
4856
|
+
sessionHistory.length = 0;
|
|
4857
|
+
logger.info("[Session Debug] Session history cleared.");
|
|
4858
|
+
},
|
|
4859
|
+
/**
|
|
4860
|
+
* Show help.
|
|
4861
|
+
*/
|
|
4862
|
+
help() {
|
|
4863
|
+
logger.info(`
|
|
4864
|
+
\u{1F3AF} Session Debug API
|
|
2818
4865
|
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
|
|
2837
|
-
}
|
|
2838
|
-
function computeEffectiveWeight(learnable, userId, strategyId, salt) {
|
|
2839
|
-
const deviation = computeDeviation(userId, strategyId, salt);
|
|
2840
|
-
const spread = computeSpread(learnable.confidence);
|
|
2841
|
-
const adjustment = deviation * spread * learnable.weight;
|
|
2842
|
-
const effective = learnable.weight + adjustment;
|
|
2843
|
-
return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
|
|
2844
|
-
}
|
|
2845
|
-
async function createOrchestrationContext(user, course) {
|
|
2846
|
-
let courseConfig;
|
|
2847
|
-
try {
|
|
2848
|
-
courseConfig = await course.getCourseConfig();
|
|
2849
|
-
} catch (e) {
|
|
2850
|
-
logger.error(`[Orchestration] Failed to load course config: ${e}`);
|
|
2851
|
-
courseConfig = {
|
|
2852
|
-
name: "Unknown",
|
|
2853
|
-
description: "",
|
|
2854
|
-
public: false,
|
|
2855
|
-
deleted: false,
|
|
2856
|
-
creator: "",
|
|
2857
|
-
admins: [],
|
|
2858
|
-
moderators: [],
|
|
2859
|
-
dataShapes: [],
|
|
2860
|
-
questionTypes: [],
|
|
2861
|
-
orchestration: { salt: "default" }
|
|
4866
|
+
Commands:
|
|
4867
|
+
.showQueue() Show current queue state (active session only)
|
|
4868
|
+
.showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
|
|
4869
|
+
.showInterleaving(index?) Analyze course interleaving pattern
|
|
4870
|
+
.listSessions() List all tracked sessions
|
|
4871
|
+
.export() Export session data as JSON for bug reports
|
|
4872
|
+
.clear() Clear session history
|
|
4873
|
+
.sessions Access raw session history array
|
|
4874
|
+
.active Access active session (if any)
|
|
4875
|
+
.help() Show this help message
|
|
4876
|
+
|
|
4877
|
+
Example:
|
|
4878
|
+
window.skuilder.session.showHistory()
|
|
4879
|
+
window.skuilder.session.showInterleaving()
|
|
4880
|
+
window.skuilder.session.showQueue()
|
|
4881
|
+
`);
|
|
4882
|
+
}
|
|
2862
4883
|
};
|
|
4884
|
+
mountSessionDebugger();
|
|
2863
4885
|
}
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
userId,
|
|
2870
|
-
courseConfig,
|
|
2871
|
-
getEffectiveWeight(strategyId, learnable) {
|
|
2872
|
-
return computeEffectiveWeight(learnable, userId, strategyId, salt);
|
|
2873
|
-
},
|
|
2874
|
-
getDeviation(strategyId) {
|
|
2875
|
-
return computeDeviation(userId, strategyId, salt);
|
|
2876
|
-
}
|
|
2877
|
-
};
|
|
2878
|
-
}
|
|
2879
|
-
var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
|
|
2880
|
-
var init_orchestration = __esm({
|
|
2881
|
-
"src/core/orchestration/index.ts"() {
|
|
4886
|
+
});
|
|
4887
|
+
|
|
4888
|
+
// src/study/SessionController.ts
|
|
4889
|
+
var init_SessionController = __esm({
|
|
4890
|
+
"src/study/SessionController.ts"() {
|
|
2882
4891
|
"use strict";
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
|
|
2886
|
-
|
|
4892
|
+
init_SrsService();
|
|
4893
|
+
init_EloService();
|
|
4894
|
+
init_ResponseProcessor();
|
|
4895
|
+
init_CardHydrationService();
|
|
4896
|
+
init_ItemQueue();
|
|
4897
|
+
init_couch();
|
|
2887
4898
|
init_recording();
|
|
2888
|
-
|
|
2889
|
-
|
|
2890
|
-
|
|
2891
|
-
|
|
4899
|
+
init_util2();
|
|
4900
|
+
init_navigators();
|
|
4901
|
+
init_SourceMixer();
|
|
4902
|
+
init_MixerDebugger();
|
|
4903
|
+
init_SessionDebugger();
|
|
4904
|
+
init_logger();
|
|
2892
4905
|
}
|
|
2893
4906
|
});
|
|
2894
4907
|
|
|
@@ -2972,15 +4985,16 @@ function logCardProvenance(cards, maxCards = 3) {
|
|
|
2972
4985
|
}
|
|
2973
4986
|
}
|
|
2974
4987
|
}
|
|
2975
|
-
var
|
|
4988
|
+
var import_common12, VERBOSE_RESULTS, Pipeline;
|
|
2976
4989
|
var init_Pipeline = __esm({
|
|
2977
4990
|
"src/core/navigators/Pipeline.ts"() {
|
|
2978
4991
|
"use strict";
|
|
2979
|
-
|
|
4992
|
+
import_common12 = require("@vue-skuilder/common");
|
|
2980
4993
|
init_navigators();
|
|
2981
4994
|
init_logger();
|
|
2982
4995
|
init_orchestration();
|
|
2983
4996
|
init_PipelineDebugger();
|
|
4997
|
+
init_SessionController();
|
|
2984
4998
|
VERBOSE_RESULTS = true;
|
|
2985
4999
|
Pipeline = class extends ContentNavigator {
|
|
2986
5000
|
generator;
|
|
@@ -3140,8 +5154,9 @@ var init_Pipeline = __esm({
|
|
|
3140
5154
|
generatorSummaries,
|
|
3141
5155
|
generatedCount,
|
|
3142
5156
|
filterImpacts,
|
|
3143
|
-
|
|
3144
|
-
result
|
|
5157
|
+
cards,
|
|
5158
|
+
result,
|
|
5159
|
+
context.userElo
|
|
3145
5160
|
);
|
|
3146
5161
|
captureRun(report);
|
|
3147
5162
|
} catch (e) {
|
|
@@ -3224,7 +5239,7 @@ var init_Pipeline = __esm({
|
|
|
3224
5239
|
card.provenance.push({
|
|
3225
5240
|
strategy: "ephemeralHint",
|
|
3226
5241
|
strategyId: "ephemeral-hint",
|
|
3227
|
-
strategyName: "Replan Hint",
|
|
5242
|
+
strategyName: hints._label ? `Replan Hint (${hints._label})` : "Replan Hint",
|
|
3228
5243
|
action: "boosted",
|
|
3229
5244
|
score: card.score,
|
|
3230
5245
|
reason: `boostTag ${pattern} \xD7${factor}`
|
|
@@ -3241,7 +5256,7 @@ var init_Pipeline = __esm({
|
|
|
3241
5256
|
card.provenance.push({
|
|
3242
5257
|
strategy: "ephemeralHint",
|
|
3243
5258
|
strategyId: "ephemeral-hint",
|
|
3244
|
-
strategyName: "Replan Hint",
|
|
5259
|
+
strategyName: hints._label ? `Replan Hint (${hints._label})` : "Replan Hint",
|
|
3245
5260
|
action: "boosted",
|
|
3246
5261
|
score: card.score,
|
|
3247
5262
|
reason: `boostCard ${pattern} \xD7${factor}`
|
|
@@ -3251,6 +5266,7 @@ var init_Pipeline = __esm({
|
|
|
3251
5266
|
}
|
|
3252
5267
|
}
|
|
3253
5268
|
const cardIds = new Set(cards.map((c) => c.cardId));
|
|
5269
|
+
const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
|
|
3254
5270
|
const inject = (card, reason) => {
|
|
3255
5271
|
if (!cardIds.has(card.cardId)) {
|
|
3256
5272
|
const floorScore = Math.max(card.score, 1);
|
|
@@ -3262,7 +5278,7 @@ var init_Pipeline = __esm({
|
|
|
3262
5278
|
{
|
|
3263
5279
|
strategy: "ephemeralHint",
|
|
3264
5280
|
strategyId: "ephemeral-hint",
|
|
3265
|
-
strategyName:
|
|
5281
|
+
strategyName: hintLabel,
|
|
3266
5282
|
action: "boosted",
|
|
3267
5283
|
score: floorScore,
|
|
3268
5284
|
reason
|
|
@@ -3301,7 +5317,7 @@ var init_Pipeline = __esm({
|
|
|
3301
5317
|
let userElo = 1e3;
|
|
3302
5318
|
try {
|
|
3303
5319
|
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
3304
|
-
const courseElo = (0,
|
|
5320
|
+
const courseElo = (0, import_common12.toCourseElo)(courseReg.elo);
|
|
3305
5321
|
userElo = courseElo.global.score;
|
|
3306
5322
|
} catch (e) {
|
|
3307
5323
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
@@ -3354,6 +5370,34 @@ var init_Pipeline = __esm({
|
|
|
3354
5370
|
return [...new Set(ids)];
|
|
3355
5371
|
}
|
|
3356
5372
|
// ---------------------------------------------------------------------------
|
|
5373
|
+
// Tag ELO diagnostic
|
|
5374
|
+
// ---------------------------------------------------------------------------
|
|
5375
|
+
/**
|
|
5376
|
+
* Get the user's per-tag ELO data for specified tags (or all tags).
|
|
5377
|
+
* Useful for diagnosing why hierarchy gates are open/closed.
|
|
5378
|
+
*/
|
|
5379
|
+
async getTagEloStatus(tagFilter) {
|
|
5380
|
+
const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
|
|
5381
|
+
const courseElo = (0, import_common12.toCourseElo)(courseReg.elo);
|
|
5382
|
+
const result = {};
|
|
5383
|
+
if (!tagFilter) {
|
|
5384
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
5385
|
+
result[tag] = { score: data.score, count: data.count };
|
|
5386
|
+
}
|
|
5387
|
+
} else {
|
|
5388
|
+
const patterns = Array.isArray(tagFilter) ? tagFilter : [tagFilter];
|
|
5389
|
+
for (const pattern of patterns) {
|
|
5390
|
+
const regex = globToRegex(pattern);
|
|
5391
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
5392
|
+
if (regex.test(tag)) {
|
|
5393
|
+
result[tag] = { score: data.score, count: data.count };
|
|
5394
|
+
}
|
|
5395
|
+
}
|
|
5396
|
+
}
|
|
5397
|
+
}
|
|
5398
|
+
return result;
|
|
5399
|
+
}
|
|
5400
|
+
// ---------------------------------------------------------------------------
|
|
3357
5401
|
// Card-space diagnostic
|
|
3358
5402
|
// ---------------------------------------------------------------------------
|
|
3359
5403
|
/**
|
|
@@ -4007,11 +6051,11 @@ ${JSON.stringify(config)}
|
|
|
4007
6051
|
function isSuccessRow(row) {
|
|
4008
6052
|
return "doc" in row && row.doc !== null && row.doc !== void 0;
|
|
4009
6053
|
}
|
|
4010
|
-
var
|
|
6054
|
+
var import_common13, CourseDB;
|
|
4011
6055
|
var init_courseDB = __esm({
|
|
4012
6056
|
"src/impl/couch/courseDB.ts"() {
|
|
4013
6057
|
"use strict";
|
|
4014
|
-
|
|
6058
|
+
import_common13 = require("@vue-skuilder/common");
|
|
4015
6059
|
init_couch();
|
|
4016
6060
|
init_updateQueue();
|
|
4017
6061
|
init_types_legacy();
|
|
@@ -4115,14 +6159,14 @@ var init_courseDB = __esm({
|
|
|
4115
6159
|
docs.rows.forEach((r) => {
|
|
4116
6160
|
if (isSuccessRow(r)) {
|
|
4117
6161
|
if (r.doc && r.doc.elo) {
|
|
4118
|
-
ret.push((0,
|
|
6162
|
+
ret.push((0, import_common13.toCourseElo)(r.doc.elo));
|
|
4119
6163
|
} else {
|
|
4120
6164
|
logger.warn("no elo data for card: " + r.id);
|
|
4121
|
-
ret.push((0,
|
|
6165
|
+
ret.push((0, import_common13.blankCourseElo)());
|
|
4122
6166
|
}
|
|
4123
6167
|
} else {
|
|
4124
6168
|
logger.warn("no elo data for card: " + JSON.stringify(r));
|
|
4125
|
-
ret.push((0,
|
|
6169
|
+
ret.push((0, import_common13.blankCourseElo)());
|
|
4126
6170
|
}
|
|
4127
6171
|
});
|
|
4128
6172
|
return ret;
|
|
@@ -4324,7 +6368,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
4324
6368
|
async getCourseTagStubs() {
|
|
4325
6369
|
return getCourseTagStubs(this.id);
|
|
4326
6370
|
}
|
|
4327
|
-
async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0,
|
|
6371
|
+
async addNote(codeCourse, shape, data, author, tags, uploads, elo = (0, import_common13.blankCourseElo)()) {
|
|
4328
6372
|
try {
|
|
4329
6373
|
const resp = await addNote55(this.id, codeCourse, shape, data, author, tags, uploads, elo);
|
|
4330
6374
|
if (resp.ok) {
|
|
@@ -4333,19 +6377,19 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
4333
6377
|
`[courseDB.addNote] Note added but card creation failed: ${resp.cardCreationError}`
|
|
4334
6378
|
);
|
|
4335
6379
|
return {
|
|
4336
|
-
status:
|
|
6380
|
+
status: import_common13.Status.error,
|
|
4337
6381
|
message: `Note was added but no cards were created: ${resp.cardCreationError}`,
|
|
4338
6382
|
id: resp.id
|
|
4339
6383
|
};
|
|
4340
6384
|
}
|
|
4341
6385
|
return {
|
|
4342
|
-
status:
|
|
6386
|
+
status: import_common13.Status.ok,
|
|
4343
6387
|
message: "",
|
|
4344
6388
|
id: resp.id
|
|
4345
6389
|
};
|
|
4346
6390
|
} else {
|
|
4347
6391
|
return {
|
|
4348
|
-
status:
|
|
6392
|
+
status: import_common13.Status.error,
|
|
4349
6393
|
message: "Unexpected error adding note"
|
|
4350
6394
|
};
|
|
4351
6395
|
}
|
|
@@ -4357,7 +6401,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
4357
6401
|
message: ${err.message}`
|
|
4358
6402
|
);
|
|
4359
6403
|
return {
|
|
4360
|
-
status:
|
|
6404
|
+
status: import_common13.Status.error,
|
|
4361
6405
|
message: `Error adding note to course. ${e.reason || err.message}`
|
|
4362
6406
|
};
|
|
4363
6407
|
}
|
|
@@ -4466,10 +6510,18 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
4466
6510
|
* @param limit - Maximum number of cards to return
|
|
4467
6511
|
* @returns Cards sorted by score descending
|
|
4468
6512
|
*/
|
|
6513
|
+
_pendingHints = null;
|
|
6514
|
+
setEphemeralHints(hints) {
|
|
6515
|
+
this._pendingHints = hints;
|
|
6516
|
+
}
|
|
4469
6517
|
async getWeightedCards(limit) {
|
|
4470
6518
|
const u = await this._getCurrentUser();
|
|
4471
6519
|
try {
|
|
4472
6520
|
const navigator = await this.createNavigator(u);
|
|
6521
|
+
if (this._pendingHints) {
|
|
6522
|
+
navigator.setEphemeralHints(this._pendingHints);
|
|
6523
|
+
this._pendingHints = null;
|
|
6524
|
+
}
|
|
4473
6525
|
return navigator.getWeightedCards(limit);
|
|
4474
6526
|
} catch (e) {
|
|
4475
6527
|
logger.error(`[courseDB] Error getting weighted cards: ${e}`);
|
|
@@ -4488,7 +6540,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
4488
6540
|
const courseDoc = (await u.getCourseRegistrationsDoc()).courses.find((c) => {
|
|
4489
6541
|
return c.courseID === this.id;
|
|
4490
6542
|
});
|
|
4491
|
-
targetElo = (0,
|
|
6543
|
+
targetElo = (0, import_common13.EloToNumber)(courseDoc.elo);
|
|
4492
6544
|
} catch {
|
|
4493
6545
|
targetElo = 1e3;
|
|
4494
6546
|
}
|
|
@@ -4612,13 +6664,13 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
4612
6664
|
});
|
|
4613
6665
|
|
|
4614
6666
|
// src/impl/couch/classroomDB.ts
|
|
4615
|
-
var
|
|
6667
|
+
var import_moment6, CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB;
|
|
4616
6668
|
var init_classroomDB2 = __esm({
|
|
4617
6669
|
"src/impl/couch/classroomDB.ts"() {
|
|
4618
6670
|
"use strict";
|
|
4619
6671
|
init_factory();
|
|
4620
6672
|
init_logger();
|
|
4621
|
-
|
|
6673
|
+
import_moment6 = __toESM(require("moment"), 1);
|
|
4622
6674
|
init_pouchdb_setup();
|
|
4623
6675
|
init_couch();
|
|
4624
6676
|
init_courseDB();
|
|
@@ -4730,9 +6782,9 @@ var init_classroomDB2 = __esm({
|
|
|
4730
6782
|
}
|
|
4731
6783
|
const activeCards = await this._user.getActiveCards();
|
|
4732
6784
|
const activeCardIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
4733
|
-
const now =
|
|
6785
|
+
const now = import_moment6.default.utc();
|
|
4734
6786
|
const assigned = await this.getAssignedContent();
|
|
4735
|
-
const due = assigned.filter((c) => now.isAfter(
|
|
6787
|
+
const due = assigned.filter((c) => now.isAfter(import_moment6.default.utc(c.activeOn, REVIEW_TIME_FORMAT2)));
|
|
4736
6788
|
logger.info(`[StudentClassroomDB] Due content: ${JSON.stringify(due)}`);
|
|
4737
6789
|
for (const content of due) {
|
|
4738
6790
|
if (content.type === "course") {
|
|
@@ -4841,14 +6893,14 @@ var init_auth = __esm({
|
|
|
4841
6893
|
});
|
|
4842
6894
|
|
|
4843
6895
|
// src/impl/couch/CouchDBSyncStrategy.ts
|
|
4844
|
-
var
|
|
6896
|
+
var import_common14;
|
|
4845
6897
|
var init_CouchDBSyncStrategy = __esm({
|
|
4846
6898
|
"src/impl/couch/CouchDBSyncStrategy.ts"() {
|
|
4847
6899
|
"use strict";
|
|
4848
6900
|
init_factory();
|
|
4849
6901
|
init_types_legacy();
|
|
4850
6902
|
init_logger();
|
|
4851
|
-
|
|
6903
|
+
import_common14 = require("@vue-skuilder/common");
|
|
4852
6904
|
init_common();
|
|
4853
6905
|
init_pouchdb_setup();
|
|
4854
6906
|
init_couch();
|
|
@@ -4899,14 +6951,14 @@ function getStartAndEndKeys2(key) {
|
|
|
4899
6951
|
endkey: key + "\uFFF0"
|
|
4900
6952
|
};
|
|
4901
6953
|
}
|
|
4902
|
-
var import_cross_fetch2,
|
|
6954
|
+
var import_cross_fetch2, import_moment7, import_process, isBrowser, GUEST_LOCAL_DB, localUserDB, pouchDBincludeCredentialsConfig, REVIEW_TIME_FORMAT2;
|
|
4903
6955
|
var init_couch = __esm({
|
|
4904
6956
|
"src/impl/couch/index.ts"() {
|
|
4905
6957
|
"use strict";
|
|
4906
6958
|
init_factory();
|
|
4907
6959
|
init_types_legacy();
|
|
4908
6960
|
import_cross_fetch2 = __toESM(require("cross-fetch"), 1);
|
|
4909
|
-
|
|
6961
|
+
import_moment7 = __toESM(require("moment"), 1);
|
|
4910
6962
|
init_logger();
|
|
4911
6963
|
init_pouchdb_setup();
|
|
4912
6964
|
import_process = __toESM(require("process"), 1);
|
|
@@ -5026,14 +7078,14 @@ async function dropUserFromClassroom(user, classID) {
|
|
|
5026
7078
|
async function getUserClassrooms(user) {
|
|
5027
7079
|
return getOrCreateClassroomRegistrationsDoc(user);
|
|
5028
7080
|
}
|
|
5029
|
-
var
|
|
7081
|
+
var import_common16, import_moment8, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
|
|
5030
7082
|
var init_BaseUserDB = __esm({
|
|
5031
7083
|
"src/impl/common/BaseUserDB.ts"() {
|
|
5032
7084
|
"use strict";
|
|
5033
7085
|
init_core();
|
|
5034
7086
|
init_util();
|
|
5035
|
-
|
|
5036
|
-
|
|
7087
|
+
import_common16 = require("@vue-skuilder/common");
|
|
7088
|
+
import_moment8 = __toESM(require("moment"), 1);
|
|
5037
7089
|
init_types_legacy();
|
|
5038
7090
|
init_logger();
|
|
5039
7091
|
init_userDBHelpers();
|
|
@@ -5082,7 +7134,7 @@ Currently logged-in as ${this._username}.`
|
|
|
5082
7134
|
);
|
|
5083
7135
|
}
|
|
5084
7136
|
const result = await this.syncStrategy.createAccount(username, password);
|
|
5085
|
-
if (result.status ===
|
|
7137
|
+
if (result.status === import_common16.Status.ok) {
|
|
5086
7138
|
log3(`Account created successfully, updating username to ${username}`);
|
|
5087
7139
|
this._username = username;
|
|
5088
7140
|
try {
|
|
@@ -5124,7 +7176,7 @@ Currently logged-in as ${this._username}.`
|
|
|
5124
7176
|
async resetUserData() {
|
|
5125
7177
|
if (this.syncStrategy.canAuthenticate()) {
|
|
5126
7178
|
return {
|
|
5127
|
-
status:
|
|
7179
|
+
status: import_common16.Status.error,
|
|
5128
7180
|
error: "Reset user data is only available for local-only mode. Use logout instead for remote sync."
|
|
5129
7181
|
};
|
|
5130
7182
|
}
|
|
@@ -5146,11 +7198,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5146
7198
|
await localDB.bulkDocs(docsToDelete);
|
|
5147
7199
|
}
|
|
5148
7200
|
await this.init();
|
|
5149
|
-
return { status:
|
|
7201
|
+
return { status: import_common16.Status.ok };
|
|
5150
7202
|
} catch (error) {
|
|
5151
7203
|
logger.error("Failed to reset user data:", error);
|
|
5152
7204
|
return {
|
|
5153
|
-
status:
|
|
7205
|
+
status: import_common16.Status.error,
|
|
5154
7206
|
error: error instanceof Error ? error.message : "Unknown error during reset"
|
|
5155
7207
|
};
|
|
5156
7208
|
}
|
|
@@ -5297,7 +7349,7 @@ Currently logged-in as ${this._username}.`
|
|
|
5297
7349
|
);
|
|
5298
7350
|
return reviews.rows.filter((r) => {
|
|
5299
7351
|
if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
|
|
5300
|
-
const date =
|
|
7352
|
+
const date = import_moment8.default.utc(
|
|
5301
7353
|
r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
|
|
5302
7354
|
REVIEW_TIME_FORMAT
|
|
5303
7355
|
);
|
|
@@ -5310,11 +7362,11 @@ Currently logged-in as ${this._username}.`
|
|
|
5310
7362
|
}).map((r) => r.doc);
|
|
5311
7363
|
}
|
|
5312
7364
|
async getReviewsForcast(daysCount) {
|
|
5313
|
-
const time =
|
|
7365
|
+
const time = import_moment8.default.utc().add(daysCount, "days");
|
|
5314
7366
|
return this.getReviewstoDate(time);
|
|
5315
7367
|
}
|
|
5316
7368
|
async getPendingReviews(course_id) {
|
|
5317
|
-
const now =
|
|
7369
|
+
const now = import_moment8.default.utc();
|
|
5318
7370
|
return this.getReviewstoDate(now, course_id);
|
|
5319
7371
|
}
|
|
5320
7372
|
async getScheduledReviewCount(course_id) {
|
|
@@ -5601,7 +7653,7 @@ Currently logged-in as ${this._username}.`
|
|
|
5601
7653
|
*/
|
|
5602
7654
|
async putCardRecord(record) {
|
|
5603
7655
|
const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
|
|
5604
|
-
record.timeStamp =
|
|
7656
|
+
record.timeStamp = import_moment8.default.utc(record.timeStamp).toString();
|
|
5605
7657
|
try {
|
|
5606
7658
|
const cardHistory = await this.update(
|
|
5607
7659
|
cardHistoryID,
|
|
@@ -5617,7 +7669,7 @@ Currently logged-in as ${this._username}.`
|
|
|
5617
7669
|
const ret = {
|
|
5618
7670
|
...record2
|
|
5619
7671
|
};
|
|
5620
|
-
ret.timeStamp =
|
|
7672
|
+
ret.timeStamp = import_moment8.default.utc(record2.timeStamp);
|
|
5621
7673
|
return ret;
|
|
5622
7674
|
});
|
|
5623
7675
|
return cardHistory;
|
|
@@ -5944,11 +7996,11 @@ var init_factory = __esm({
|
|
|
5944
7996
|
});
|
|
5945
7997
|
|
|
5946
7998
|
// src/study/TagFilteredContentSource.ts
|
|
5947
|
-
var
|
|
7999
|
+
var import_common18, TagFilteredContentSource;
|
|
5948
8000
|
var init_TagFilteredContentSource = __esm({
|
|
5949
8001
|
"src/study/TagFilteredContentSource.ts"() {
|
|
5950
8002
|
"use strict";
|
|
5951
|
-
|
|
8003
|
+
import_common18 = require("@vue-skuilder/common");
|
|
5952
8004
|
init_courseDB();
|
|
5953
8005
|
init_logger();
|
|
5954
8006
|
TagFilteredContentSource = class {
|
|
@@ -6034,7 +8086,7 @@ var init_TagFilteredContentSource = __esm({
|
|
|
6034
8086
|
* @returns Cards sorted by score descending (all scores = 1.0)
|
|
6035
8087
|
*/
|
|
6036
8088
|
async getWeightedCards(limit) {
|
|
6037
|
-
if (!(0,
|
|
8089
|
+
if (!(0, import_common18.hasActiveFilter)(this.filter)) {
|
|
6038
8090
|
logger.warn("[TagFilteredContentSource] getWeightedCards called with no active filter");
|
|
6039
8091
|
return [];
|
|
6040
8092
|
}
|
|
@@ -6122,19 +8174,19 @@ async function getStudySource(source, user) {
|
|
|
6122
8174
|
if (source.type === "classroom") {
|
|
6123
8175
|
return await StudentClassroomDB.factory(source.id, user);
|
|
6124
8176
|
} else {
|
|
6125
|
-
if ((0,
|
|
8177
|
+
if ((0, import_common19.hasActiveFilter)(source.tagFilter)) {
|
|
6126
8178
|
return new TagFilteredContentSource(source.id, source.tagFilter, user);
|
|
6127
8179
|
}
|
|
6128
8180
|
return getDataLayer().getCourseDB(source.id);
|
|
6129
8181
|
}
|
|
6130
8182
|
}
|
|
6131
|
-
var
|
|
8183
|
+
var import_common19;
|
|
6132
8184
|
var init_contentSource = __esm({
|
|
6133
8185
|
"src/core/interfaces/contentSource.ts"() {
|
|
6134
8186
|
"use strict";
|
|
6135
8187
|
init_factory();
|
|
6136
8188
|
init_classroomDB2();
|
|
6137
|
-
|
|
8189
|
+
import_common19 = require("@vue-skuilder/common");
|
|
6138
8190
|
init_TagFilteredContentSource();
|
|
6139
8191
|
}
|
|
6140
8192
|
});
|
|
@@ -6265,7 +8317,7 @@ elo: ${elo}`;
|
|
|
6265
8317
|
misc: {}
|
|
6266
8318
|
} : void 0
|
|
6267
8319
|
);
|
|
6268
|
-
if (result.status ===
|
|
8320
|
+
if (result.status === import_common20.Status.ok) {
|
|
6269
8321
|
return {
|
|
6270
8322
|
originalText,
|
|
6271
8323
|
status: "success",
|
|
@@ -6309,17 +8361,17 @@ function validateProcessorConfig(config) {
|
|
|
6309
8361
|
}
|
|
6310
8362
|
return { isValid: true };
|
|
6311
8363
|
}
|
|
6312
|
-
var
|
|
8364
|
+
var import_common20;
|
|
6313
8365
|
var init_cardProcessor = __esm({
|
|
6314
8366
|
"src/core/bulkImport/cardProcessor.ts"() {
|
|
6315
8367
|
"use strict";
|
|
6316
|
-
|
|
8368
|
+
import_common20 = require("@vue-skuilder/common");
|
|
6317
8369
|
init_logger();
|
|
6318
8370
|
}
|
|
6319
8371
|
});
|
|
6320
8372
|
|
|
6321
8373
|
// src/core/bulkImport/types.ts
|
|
6322
|
-
var
|
|
8374
|
+
var init_types5 = __esm({
|
|
6323
8375
|
"src/core/bulkImport/types.ts"() {
|
|
6324
8376
|
"use strict";
|
|
6325
8377
|
}
|
|
@@ -6330,7 +8382,7 @@ var init_bulkImport = __esm({
|
|
|
6330
8382
|
"src/core/bulkImport/index.ts"() {
|
|
6331
8383
|
"use strict";
|
|
6332
8384
|
init_cardProcessor();
|
|
6333
|
-
|
|
8385
|
+
init_types5();
|
|
6334
8386
|
}
|
|
6335
8387
|
});
|
|
6336
8388
|
|