@vue-skuilder/db 0.1.32-b → 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.
Files changed (50) hide show
  1. package/dist/core/index.d.cts +16 -12
  2. package/dist/core/index.d.ts +16 -12
  3. package/dist/core/index.js +2262 -223
  4. package/dist/core/index.js.map +1 -1
  5. package/dist/core/index.mjs +2239 -196
  6. package/dist/core/index.mjs.map +1 -1
  7. package/dist/{contentSource-Bdwkvqa8.d.ts → dataLayerProvider-BAn-LRh5.d.ts} +626 -83
  8. package/dist/{contentSource-DF1nUbPQ.d.cts → dataLayerProvider-BJqBlMIl.d.cts} +626 -83
  9. package/dist/impl/couch/index.d.cts +17 -4
  10. package/dist/impl/couch/index.d.ts +17 -4
  11. package/dist/impl/couch/index.js +2306 -220
  12. package/dist/impl/couch/index.js.map +1 -1
  13. package/dist/impl/couch/index.mjs +2294 -204
  14. package/dist/impl/couch/index.mjs.map +1 -1
  15. package/dist/impl/static/index.d.cts +4 -5
  16. package/dist/impl/static/index.d.ts +4 -5
  17. package/dist/impl/static/index.js +2266 -227
  18. package/dist/impl/static/index.js.map +1 -1
  19. package/dist/impl/static/index.mjs +2251 -208
  20. package/dist/impl/static/index.mjs.map +1 -1
  21. package/dist/{index-BWvO-_rJ.d.ts → index-X6wHrURm.d.ts} +1 -1
  22. package/dist/{index-Ba7hYbHj.d.cts → index-m8MMGxxR.d.cts} +1 -1
  23. package/dist/index.d.cts +9 -444
  24. package/dist/index.d.ts +9 -444
  25. package/dist/index.js +9637 -8931
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +9539 -8833
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/{types-CJrLM1Ew.d.ts → types-DZ5dUqbL.d.ts} +1 -1
  30. package/dist/{types-W8n-B6HG.d.cts → types-ZL8tOPQZ.d.cts} +1 -1
  31. package/dist/{types-legacy-JXDxinpU.d.cts → types-legacy-C7r0T4OV.d.cts} +1 -1
  32. package/dist/{types-legacy-JXDxinpU.d.ts → types-legacy-C7r0T4OV.d.ts} +1 -1
  33. package/dist/util/packer/index.d.cts +3 -3
  34. package/dist/util/packer/index.d.ts +3 -3
  35. package/docs/navigators-architecture.md +2 -2
  36. package/package.json +2 -2
  37. package/src/core/interfaces/contentSource.ts +2 -1
  38. package/src/core/navigators/Pipeline.ts +47 -29
  39. package/src/core/navigators/PipelineDebugger.ts +49 -1
  40. package/src/core/navigators/filters/hierarchyDefinition.ts +88 -5
  41. package/src/core/navigators/generators/prescribed.ts +618 -43
  42. package/src/core/navigators/index.ts +2 -1
  43. package/src/impl/couch/CourseSyncService.ts +72 -4
  44. package/src/impl/couch/courseDB.ts +3 -2
  45. package/src/impl/static/courseDB.ts +3 -2
  46. package/src/study/SessionController.ts +79 -9
  47. package/src/study/services/EloService.ts +22 -3
  48. package/src/study/services/ResponseProcessor.ts +7 -3
  49. package/dist/dataLayerProvider-BKmVoyJR.d.ts +0 -67
  50. package/dist/dataLayerProvider-BQdfJuBN.d.cts +0 -67
@@ -727,13 +727,20 @@ function captureRun(report) {
727
727
  runHistory.pop();
728
728
  }
729
729
  }
730
- function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards) {
730
+ function parseCardElo(provenance) {
731
+ const eloEntry = provenance.find((p) => p.strategy === "elo");
732
+ if (!eloEntry?.reason) return void 0;
733
+ const match = eloEntry.reason.match(/card:\s*(\d+)/);
734
+ return match ? parseInt(match[1], 10) : void 0;
735
+ }
736
+ function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
731
737
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
732
738
  const cards = allCards.map((card) => ({
733
739
  cardId: card.cardId,
734
740
  courseId: card.courseId,
735
741
  origin: getOrigin(card),
736
742
  finalScore: card.score,
743
+ cardElo: parseCardElo(card.provenance),
737
744
  provenance: card.provenance,
738
745
  tags: card.tags,
739
746
  selected: selectedIds.has(card.cardId)
@@ -743,6 +750,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
743
750
  return {
744
751
  courseId,
745
752
  courseName,
753
+ userElo,
746
754
  generatorName,
747
755
  generators,
748
756
  generatedCount,
@@ -763,6 +771,7 @@ function printRunSummary(run) {
763
771
  console.group(`\u{1F50D} Pipeline Run: ${run.courseId} (${run.courseName || "unnamed"})`);
764
772
  logger.info(`Run ID: ${run.runId}`);
765
773
  logger.info(`Time: ${run.timestamp.toISOString()}`);
774
+ logger.info(`User ELO: ${run.userElo ?? "unknown"}`);
766
775
  logger.info(`Generator: ${run.generatorName} \u2192 ${run.generatedCount} candidates`);
767
776
  if (run.generators && run.generators.length > 0) {
768
777
  console.group("Generator breakdown:");
@@ -849,8 +858,12 @@ var init_PipelineDebugger = __esm({
849
858
  console.group(`\u{1F3B4} Card: ${cardId}`);
850
859
  logger.info(`Course: ${card.courseId}`);
851
860
  logger.info(`Origin: ${card.origin}`);
861
+ logger.info(`Card ELO: ${card.cardElo ?? "unknown"} | User ELO: ${run.userElo ?? "unknown"}`);
852
862
  logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
853
863
  logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
864
+ if (card.tags && card.tags.length > 0) {
865
+ logger.info(`Tags (${card.tags.length}): ${card.tags.join(", ")}`);
866
+ }
854
867
  logger.info("Provenance:");
855
868
  logger.info(formatProvenance(card.provenance));
856
869
  console.groupEnd();
@@ -1014,6 +1027,27 @@ var init_PipelineDebugger = __esm({
1014
1027
  }
1015
1028
  return _activePipeline.diagnoseCardSpace({ threshold });
1016
1029
  },
1030
+ /**
1031
+ * Show user's per-tag ELO data. Useful for diagnosing hierarchy gate status.
1032
+ *
1033
+ * @param tagFilter - Optional glob pattern(s) to filter tags.
1034
+ * Examples: 'gpc:expose:*', 'gpc:intro:t-T', ['gpc:expose:t-*', 'gpc:intro:t-*']
1035
+ */
1036
+ async showTagElo(tagFilter) {
1037
+ if (!_activePipeline) {
1038
+ logger.info("[Pipeline Debug] No active pipeline. Run a session first.");
1039
+ return;
1040
+ }
1041
+ const status = await _activePipeline.getTagEloStatus(tagFilter);
1042
+ const entries = Object.entries(status).sort(([a], [b]) => a.localeCompare(b));
1043
+ if (entries.length === 0) {
1044
+ logger.info(`[Pipeline Debug] No tag ELO data found${tagFilter ? ` for pattern: ${tagFilter}` : ""}.`);
1045
+ return;
1046
+ }
1047
+ console.table(
1048
+ Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
1049
+ );
1050
+ },
1017
1051
  /**
1018
1052
  * Show help.
1019
1053
  */
@@ -1025,6 +1059,7 @@ Commands:
1025
1059
  .showLastRun() Show summary of most recent pipeline run
1026
1060
  .showRun(id|index) Show summary of a specific run (by index or ID suffix)
1027
1061
  .showCard(cardId) Show provenance trail for a specific card
1062
+ .showTagElo(pattern) Show user's tag ELO data (async). E.g. 'gpc:expose:*'
1028
1063
  .explainReviews() Analyze why reviews were/weren't selected
1029
1064
  .diagnoseCardSpace() Scan full card space through filters (async)
1030
1065
  .showRegistry() Show navigator registry (classes + roles)
@@ -1338,60 +1373,423 @@ var prescribed_exports = {};
1338
1373
  __export(prescribed_exports, {
1339
1374
  default: () => PrescribedCardsGenerator
1340
1375
  });
1341
- var PrescribedCardsGenerator;
1376
+ function dedupe(arr) {
1377
+ return [...new Set(arr)];
1378
+ }
1379
+ function isoNow() {
1380
+ return (/* @__PURE__ */ new Date()).toISOString();
1381
+ }
1382
+ function clamp(value, min, max) {
1383
+ return Math.max(min, Math.min(max, value));
1384
+ }
1385
+ function matchesTagPattern(tag, pattern) {
1386
+ if (pattern === "*") return true;
1387
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1388
+ const re = new RegExp(`^${escaped}$`);
1389
+ return re.test(tag);
1390
+ }
1391
+ function pickTopByScore(cards, limit) {
1392
+ return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1393
+ }
1394
+ 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;
1342
1395
  var init_prescribed = __esm({
1343
1396
  "src/core/navigators/generators/prescribed.ts"() {
1344
1397
  "use strict";
1345
1398
  init_navigators();
1346
1399
  init_logger();
1400
+ DEFAULT_FRESHNESS_WINDOW = 3;
1401
+ DEFAULT_MAX_DIRECT_PER_RUN = 3;
1402
+ DEFAULT_MAX_SUPPORT_PER_RUN = 3;
1403
+ DEFAULT_HIERARCHY_DEPTH = 2;
1404
+ DEFAULT_MIN_COUNT = 3;
1405
+ BASE_TARGET_SCORE = 1;
1406
+ BASE_SUPPORT_SCORE = 0.8;
1407
+ MAX_TARGET_MULTIPLIER = 8;
1408
+ MAX_SUPPORT_MULTIPLIER = 4;
1409
+ LOCKED_TAG_PREFIXES = ["concept:"];
1410
+ LESSON_GATE_PENALTY_TAG_HINT = "concept:";
1347
1411
  PrescribedCardsGenerator = class extends ContentNavigator {
1348
1412
  name;
1349
1413
  config;
1350
1414
  constructor(user, course, strategyData) {
1351
1415
  super(user, course, strategyData);
1352
1416
  this.name = strategyData.name || "Prescribed Cards";
1353
- try {
1354
- const parsed = JSON.parse(strategyData.serializedData);
1355
- this.config = { cardIds: parsed.cardIds || [] };
1356
- } catch {
1357
- this.config = { cardIds: [] };
1358
- }
1417
+ this.config = this.parseConfig(strategyData.serializedData);
1359
1418
  logger.debug(
1360
- `[Prescribed] Initialized with ${this.config.cardIds.length} prescribed cards`
1419
+ `[Prescribed] Initialized with ${this.config.groups.length} groups and ${this.config.groups.reduce((n, g) => n + g.targetCardIds.length, 0)} targets`
1361
1420
  );
1362
1421
  }
1363
- async getWeightedCards(limit, _context) {
1364
- if (this.config.cardIds.length === 0) {
1422
+ get strategyKey() {
1423
+ return "PrescribedProgress";
1424
+ }
1425
+ async getWeightedCards(limit, context) {
1426
+ if (this.config.groups.length === 0 || limit <= 0) {
1365
1427
  return [];
1366
1428
  }
1367
1429
  const courseId = this.course.getCourseID();
1368
1430
  const activeCards = await this.user.getActiveCards();
1369
1431
  const activeIds = new Set(activeCards.map((ac) => ac.cardID));
1370
- const eligibleIds = this.config.cardIds.filter((id) => !activeIds.has(id));
1371
- if (eligibleIds.length === 0) {
1372
- logger.debug("[Prescribed] All prescribed cards already active, returning empty");
1432
+ const seenCards = await this.user.getSeenCards(courseId).catch(() => []);
1433
+ const seenIds = new Set(seenCards);
1434
+ const progress = await this.getStrategyState() ?? {
1435
+ updatedAt: isoNow(),
1436
+ groups: {}
1437
+ };
1438
+ const hierarchyConfigs = await this.loadHierarchyConfigs();
1439
+ const courseReg = await this.user.getCourseRegDoc(courseId).catch(() => null);
1440
+ const userGlobalElo = typeof courseReg?.elo === "number" ? courseReg.elo : courseReg?.elo?.global?.score ?? context?.userElo ?? 1e3;
1441
+ const userTagElo = typeof courseReg?.elo === "number" ? {} : courseReg?.elo?.tags ?? {};
1442
+ const allTargetIds = dedupe(this.config.groups.flatMap((g) => g.targetCardIds));
1443
+ const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
1444
+ const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
1445
+ const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
1446
+ const nextState = {
1447
+ updatedAt: isoNow(),
1448
+ groups: {}
1449
+ };
1450
+ const emitted = [];
1451
+ const emittedIds = /* @__PURE__ */ new Set();
1452
+ for (const group of this.config.groups) {
1453
+ const runtime = this.buildGroupRuntimeState({
1454
+ group,
1455
+ priorState: progress.groups[group.id],
1456
+ activeIds,
1457
+ seenIds,
1458
+ tagsByCard,
1459
+ hierarchyConfigs,
1460
+ userTagElo,
1461
+ userGlobalElo
1462
+ });
1463
+ nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
1464
+ const directCards = this.buildDirectTargetCards(
1465
+ runtime,
1466
+ courseId,
1467
+ emittedIds
1468
+ );
1469
+ const supportCards = this.buildSupportCards(
1470
+ runtime,
1471
+ courseId,
1472
+ emittedIds
1473
+ );
1474
+ emitted.push(...directCards, ...supportCards);
1475
+ }
1476
+ if (emitted.length === 0) {
1477
+ logger.debug("[Prescribed] No prescribed targets/support emitted this run");
1478
+ await this.putStrategyState(nextState).catch((e) => {
1479
+ logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
1480
+ });
1373
1481
  return [];
1374
1482
  }
1375
- const cards = eligibleIds.slice(0, limit).map((cardId) => ({
1376
- cardId,
1377
- courseId,
1378
- score: 1,
1379
- provenance: [
1380
- {
1381
- strategy: "prescribed",
1382
- strategyName: this.strategyName || this.name,
1383
- strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
1384
- action: "generated",
1385
- score: 1,
1386
- reason: `Prescribed card (${eligibleIds.length} eligible of ${this.config.cardIds.length} configured)`
1483
+ const finalCards = pickTopByScore(emitted, limit);
1484
+ const surfacedByGroup = /* @__PURE__ */ new Map();
1485
+ for (const card of finalCards) {
1486
+ const prov = card.provenance[0];
1487
+ const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
1488
+ const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
1489
+ if (!groupId) continue;
1490
+ if (!surfacedByGroup.has(groupId)) {
1491
+ surfacedByGroup.set(groupId, { targetIds: [], supportIds: [] });
1492
+ }
1493
+ surfacedByGroup.get(groupId)[mode].push(card.cardId);
1494
+ }
1495
+ for (const group of this.config.groups) {
1496
+ const groupState = nextState.groups[group.id];
1497
+ const surfaced = surfacedByGroup.get(group.id);
1498
+ if (surfaced && (surfaced.targetIds.length > 0 || surfaced.supportIds.length > 0)) {
1499
+ groupState.lastSurfacedAt = isoNow();
1500
+ groupState.sessionsSinceSurfaced = 0;
1501
+ if (surfaced.supportIds.length > 0) {
1502
+ groupState.lastSupportAt = isoNow();
1387
1503
  }
1388
- ]
1389
- }));
1504
+ }
1505
+ }
1506
+ await this.putStrategyState(nextState).catch((e) => {
1507
+ logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
1508
+ });
1390
1509
  logger.info(
1391
- `[Prescribed] Emitting ${cards.length} cards (${eligibleIds.length} eligible, ${activeIds.size} already active)`
1510
+ `[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)`
1392
1511
  );
1512
+ return finalCards;
1513
+ }
1514
+ parseConfig(serializedData) {
1515
+ try {
1516
+ const parsed = JSON.parse(serializedData);
1517
+ const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
1518
+ const groups = groupsRaw.map((raw, i) => ({
1519
+ id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
1520
+ targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
1521
+ supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
1522
+ supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
1523
+ freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
1524
+ maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
1525
+ maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
1526
+ hierarchyWalk: {
1527
+ enabled: raw.hierarchyWalk?.enabled !== false,
1528
+ maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
1529
+ },
1530
+ retireOnEncounter: raw.retireOnEncounter !== false
1531
+ })).filter((g) => g.targetCardIds.length > 0);
1532
+ return { groups };
1533
+ } catch {
1534
+ return { groups: [] };
1535
+ }
1536
+ }
1537
+ async loadHierarchyConfigs() {
1538
+ try {
1539
+ const strategies = await this.course.getNavigationStrategies();
1540
+ return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
1541
+ try {
1542
+ const parsed = JSON.parse(s.serializedData);
1543
+ return {
1544
+ prerequisites: parsed.prerequisites || {}
1545
+ };
1546
+ } catch {
1547
+ return { prerequisites: {} };
1548
+ }
1549
+ });
1550
+ } catch (e) {
1551
+ logger.debug(`[Prescribed] Failed to load hierarchy configs: ${e}`);
1552
+ return [];
1553
+ }
1554
+ }
1555
+ buildGroupRuntimeState(args) {
1556
+ const {
1557
+ group,
1558
+ priorState,
1559
+ activeIds,
1560
+ seenIds,
1561
+ tagsByCard,
1562
+ hierarchyConfigs,
1563
+ userTagElo,
1564
+ userGlobalElo
1565
+ } = args;
1566
+ const encounteredTargets = /* @__PURE__ */ new Set();
1567
+ for (const cardId of group.targetCardIds) {
1568
+ if (activeIds.has(cardId) || seenIds.has(cardId)) {
1569
+ encounteredTargets.add(cardId);
1570
+ }
1571
+ }
1572
+ if (priorState?.encounteredCardIds?.length) {
1573
+ for (const cardId of priorState.encounteredCardIds) {
1574
+ encounteredTargets.add(cardId);
1575
+ }
1576
+ }
1577
+ const pendingTargets = group.targetCardIds.filter((id) => !encounteredTargets.has(id));
1578
+ const targetTags = /* @__PURE__ */ new Map();
1579
+ for (const cardId of pendingTargets) {
1580
+ targetTags.set(cardId, tagsByCard.get(cardId) ?? []);
1581
+ }
1582
+ const blockedTargets = [];
1583
+ const surfaceableTargets = [];
1584
+ const supportTags = /* @__PURE__ */ new Set();
1585
+ for (const cardId of pendingTargets) {
1586
+ const tags = targetTags.get(cardId) ?? [];
1587
+ const resolution = this.resolveBlockedSupportTags(
1588
+ tags,
1589
+ hierarchyConfigs,
1590
+ userTagElo,
1591
+ userGlobalElo,
1592
+ group.hierarchyWalk?.enabled !== false,
1593
+ group.hierarchyWalk?.maxDepth ?? DEFAULT_HIERARCHY_DEPTH
1594
+ );
1595
+ if (resolution.blocked) {
1596
+ blockedTargets.push(cardId);
1597
+ resolution.supportTags.forEach((t) => supportTags.add(t));
1598
+ } else {
1599
+ surfaceableTargets.push(cardId);
1600
+ }
1601
+ }
1602
+ const supportCandidates = dedupe([
1603
+ ...group.supportCardIds ?? [],
1604
+ ...this.findSupportCardsByTags(
1605
+ group,
1606
+ tagsByCard,
1607
+ [...supportTags]
1608
+ )
1609
+ ]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
1610
+ const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
1611
+ const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
1612
+ const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
1613
+ const pressureMultiplier = pendingTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.75 + Math.min(2, pendingTargets.length * 0.1), 1, MAX_TARGET_MULTIPLIER);
1614
+ const supportMultiplier = blockedTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.5 + Math.min(1.5, blockedTargets.length * 0.15), 1, MAX_SUPPORT_MULTIPLIER);
1615
+ return {
1616
+ group,
1617
+ encounteredTargets,
1618
+ pendingTargets,
1619
+ blockedTargets,
1620
+ surfaceableTargets,
1621
+ targetTags,
1622
+ supportCandidates,
1623
+ supportTags: [...supportTags],
1624
+ pressureMultiplier,
1625
+ supportMultiplier
1626
+ };
1627
+ }
1628
+ buildNextGroupState(runtime, prior) {
1629
+ const carriedSessions = prior?.sessionsSinceSurfaced ?? 0;
1630
+ const surfacedThisRun = false;
1631
+ return {
1632
+ encounteredCardIds: [...runtime.encounteredTargets].sort(),
1633
+ lastSurfacedAt: prior?.lastSurfacedAt ?? null,
1634
+ sessionsSinceSurfaced: surfacedThisRun ? 0 : carriedSessions + 1,
1635
+ lastSupportAt: prior?.lastSupportAt ?? null,
1636
+ blockedTargetIds: [...runtime.blockedTargets].sort(),
1637
+ lastResolvedSupportTags: [...runtime.supportTags].sort()
1638
+ };
1639
+ }
1640
+ buildDirectTargetCards(runtime, courseId, emittedIds) {
1641
+ const maxDirect = runtime.group.maxDirectTargetsPerRun ?? DEFAULT_MAX_DIRECT_PER_RUN;
1642
+ const directIds = runtime.surfaceableTargets.filter((id) => !emittedIds.has(id)).slice(0, maxDirect);
1643
+ const cards = [];
1644
+ for (const cardId of directIds) {
1645
+ emittedIds.add(cardId);
1646
+ cards.push({
1647
+ cardId,
1648
+ courseId,
1649
+ score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
1650
+ provenance: [
1651
+ {
1652
+ strategy: "prescribed",
1653
+ strategyName: this.strategyName || this.name,
1654
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
1655
+ action: "generated",
1656
+ score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
1657
+ reason: `mode=target;group=${runtime.group.id};pending=${runtime.pendingTargets.length};surfaceable=${runtime.surfaceableTargets.length};blocked=${runtime.blockedTargets.length};multiplier=${runtime.pressureMultiplier.toFixed(2)}`
1658
+ }
1659
+ ]
1660
+ });
1661
+ }
1662
+ return cards;
1663
+ }
1664
+ buildSupportCards(runtime, courseId, emittedIds) {
1665
+ if (runtime.blockedTargets.length === 0 || runtime.supportCandidates.length === 0) {
1666
+ return [];
1667
+ }
1668
+ const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
1669
+ const supportIds = runtime.supportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
1670
+ const cards = [];
1671
+ for (const cardId of supportIds) {
1672
+ emittedIds.add(cardId);
1673
+ cards.push({
1674
+ cardId,
1675
+ courseId,
1676
+ score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
1677
+ provenance: [
1678
+ {
1679
+ strategy: "prescribed",
1680
+ strategyName: this.strategyName || this.name,
1681
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
1682
+ action: "generated",
1683
+ score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
1684
+ reason: `mode=support;group=${runtime.group.id};blocked=${runtime.blockedTargets.length};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.supportMultiplier.toFixed(2)}`
1685
+ }
1686
+ ]
1687
+ });
1688
+ }
1393
1689
  return cards;
1394
1690
  }
1691
+ findSupportCardsByTags(group, tagsByCard, supportTags) {
1692
+ if (supportTags.length === 0) {
1693
+ return [];
1694
+ }
1695
+ const explicitSupportIds = group.supportCardIds ?? [];
1696
+ const explicitPatterns = group.supportTagPatterns ?? [];
1697
+ if (explicitSupportIds.length === 0 && explicitPatterns.length === 0) {
1698
+ return [];
1699
+ }
1700
+ const candidates = /* @__PURE__ */ new Set();
1701
+ for (const cardId of explicitSupportIds) {
1702
+ const cardTags = tagsByCard.get(cardId) ?? [];
1703
+ const matchesResolved = supportTags.some((supportTag) => cardTags.includes(supportTag));
1704
+ const matchesPattern = explicitPatterns.some(
1705
+ (pattern) => cardTags.some((tag) => matchesTagPattern(tag, pattern))
1706
+ );
1707
+ if (matchesResolved || matchesPattern) {
1708
+ candidates.add(cardId);
1709
+ }
1710
+ }
1711
+ return [...candidates];
1712
+ }
1713
+ resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
1714
+ if (!hierarchyWalkEnabled || targetTags.length === 0 || hierarchyConfigs.length === 0) {
1715
+ return {
1716
+ blocked: false,
1717
+ supportTags: []
1718
+ };
1719
+ }
1720
+ const supportTags = /* @__PURE__ */ new Set();
1721
+ let blocked = false;
1722
+ for (const targetTag of targetTags) {
1723
+ for (const hierarchy of hierarchyConfigs) {
1724
+ const prereqs = hierarchy.prerequisites[targetTag];
1725
+ if (!prereqs || prereqs.length === 0) continue;
1726
+ const unmet = prereqs.filter(
1727
+ (pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo)
1728
+ );
1729
+ if (unmet.length === 0) {
1730
+ continue;
1731
+ }
1732
+ blocked = true;
1733
+ for (const prereq of unmet) {
1734
+ this.collectSupportTagsRecursive(
1735
+ prereq.tag,
1736
+ hierarchyConfigs,
1737
+ userTagElo,
1738
+ userGlobalElo,
1739
+ maxDepth,
1740
+ /* @__PURE__ */ new Set(),
1741
+ supportTags
1742
+ );
1743
+ }
1744
+ }
1745
+ }
1746
+ return { blocked, supportTags: [...supportTags] };
1747
+ }
1748
+ collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
1749
+ if (depth < 0 || visited.has(tag)) return;
1750
+ if (this.isHardGatedTag(tag)) return;
1751
+ visited.add(tag);
1752
+ let walkedFurther = false;
1753
+ for (const hierarchy of hierarchyConfigs) {
1754
+ const prereqs = hierarchy.prerequisites[tag];
1755
+ if (!prereqs || prereqs.length === 0) continue;
1756
+ const unmet = prereqs.filter(
1757
+ (pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo)
1758
+ );
1759
+ if (unmet.length > 0 && depth > 0) {
1760
+ walkedFurther = true;
1761
+ for (const prereq of unmet) {
1762
+ this.collectSupportTagsRecursive(
1763
+ prereq.tag,
1764
+ hierarchyConfigs,
1765
+ userTagElo,
1766
+ userGlobalElo,
1767
+ depth - 1,
1768
+ visited,
1769
+ out
1770
+ );
1771
+ }
1772
+ }
1773
+ }
1774
+ if (!walkedFurther) {
1775
+ out.add(tag);
1776
+ }
1777
+ }
1778
+ isHardGatedTag(tag) {
1779
+ return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
1780
+ }
1781
+ isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
1782
+ if (!userTagElo) return false;
1783
+ const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
1784
+ if (userTagElo.count < minCount) return false;
1785
+ if (prereq.masteryThreshold?.minElo !== void 0) {
1786
+ return userTagElo.score >= prereq.masteryThreshold.minElo;
1787
+ }
1788
+ if (prereq.masteryThreshold?.minCount !== void 0) {
1789
+ return true;
1790
+ }
1791
+ return userTagElo.score >= userGlobalElo;
1792
+ }
1395
1793
  };
1396
1794
  }
1397
1795
  });
@@ -1755,13 +2153,13 @@ __export(hierarchyDefinition_exports, {
1755
2153
  default: () => HierarchyDefinitionNavigator
1756
2154
  });
1757
2155
  import { toCourseElo as toCourseElo3 } from "@vue-skuilder/common";
1758
- var DEFAULT_MIN_COUNT, HierarchyDefinitionNavigator;
2156
+ var DEFAULT_MIN_COUNT2, HierarchyDefinitionNavigator;
1759
2157
  var init_hierarchyDefinition = __esm({
1760
2158
  "src/core/navigators/filters/hierarchyDefinition.ts"() {
1761
2159
  "use strict";
1762
2160
  init_navigators();
1763
2161
  init_logger();
1764
- DEFAULT_MIN_COUNT = 3;
2162
+ DEFAULT_MIN_COUNT2 = 3;
1765
2163
  HierarchyDefinitionNavigator = class extends ContentNavigator {
1766
2164
  config;
1767
2165
  /** Human-readable name for CardFilter interface */
@@ -1788,7 +2186,7 @@ var init_hierarchyDefinition = __esm({
1788
2186
  */
1789
2187
  isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
1790
2188
  if (!userTagElo) return false;
1791
- const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
2189
+ const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT2;
1792
2190
  if (userTagElo.count < minCount) return false;
1793
2191
  if (prereq.masteryThreshold?.minElo !== void 0) {
1794
2192
  return userTagElo.score >= prereq.masteryThreshold.minElo;
@@ -1889,18 +2287,55 @@ var init_hierarchyDefinition = __esm({
1889
2287
  }
1890
2288
  return boosts;
1891
2289
  }
2290
+ /**
2291
+ * Build a map of gated tag → max configured targetBoost for all *open* gates.
2292
+ *
2293
+ * When a gate opens (prereqs met), cards carrying the gated tag get boosted —
2294
+ * ensuring newly-unlocked content surfaces promptly. The boost is a static
2295
+ * multiplier; natural ELO/SRS deprioritization after interaction handles decay.
2296
+ */
2297
+ getTargetBoosts(unlockedTags) {
2298
+ const boosts = /* @__PURE__ */ new Map();
2299
+ const configKeys = Object.keys(this.config.prerequisites);
2300
+ const unlockedArr = [...unlockedTags];
2301
+ logger.info(
2302
+ `[HierarchyDefinition:targetBoost:trace] ${this.name} | configKeys=${configKeys.length}, unlocked=${unlockedArr.length} (${unlockedArr.slice(0, 5).join(", ")}${unlockedArr.length > 5 ? "..." : ""})`
2303
+ );
2304
+ for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
2305
+ if (!unlockedTags.has(tagId)) continue;
2306
+ logger.info(
2307
+ `[HierarchyDefinition:targetBoost:trace] UNLOCKED ${tagId}: ${prereqs.length} prereqs, raw=${JSON.stringify(prereqs.map((p) => ({ tag: p.tag, tb: p.targetBoost })))}`
2308
+ );
2309
+ for (const prereq of prereqs) {
2310
+ if (!prereq.targetBoost || prereq.targetBoost <= 1) continue;
2311
+ const existing = boosts.get(tagId) ?? 1;
2312
+ boosts.set(tagId, Math.max(existing, prereq.targetBoost));
2313
+ }
2314
+ }
2315
+ if (boosts.size > 0) {
2316
+ logger.info(
2317
+ `[HierarchyDefinition] targetBoosts active: ${[...boosts.entries()].map(([t, b]) => `${t}=\xD7${b}`).join(", ")}`
2318
+ );
2319
+ } else {
2320
+ logger.info(
2321
+ `[HierarchyDefinition:targetBoost:trace] no targetBoosts found despite ${unlockedArr.length} unlocked tags`
2322
+ );
2323
+ }
2324
+ return boosts;
2325
+ }
1892
2326
  /**
1893
2327
  * CardFilter.transform implementation.
1894
2328
  *
1895
- * Two effects:
1896
- * 1. Cards with locked tags receive score * 0.05 (gating penalty)
1897
- * 2. Cards carrying prereq tags of closed gates receive a configured
1898
- * boost (preReqBoost), steering toward content that unlocks gates
2329
+ * Three effects:
2330
+ * 1. Cards with locked tags receive score * 0.02 (gating penalty)
2331
+ * 2. Cards carrying prereq tags of closed gates receive preReqBoost
2332
+ * 3. Cards carrying gated tags of open gates receive targetBoost
1899
2333
  */
1900
2334
  async transform(cards, context) {
1901
2335
  const masteredTags = await this.getMasteredTags(context);
1902
2336
  const unlockedTags = this.getUnlockedTags(masteredTags);
1903
2337
  const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
2338
+ const targetBoosts = this.getTargetBoosts(unlockedTags);
1904
2339
  const gated = [];
1905
2340
  for (const card of cards) {
1906
2341
  const { isUnlocked, reason } = await this.checkCardUnlock(
@@ -1933,6 +2368,26 @@ var init_hierarchyDefinition = __esm({
1933
2368
  );
1934
2369
  }
1935
2370
  }
2371
+ if (isUnlocked && targetBoosts.size > 0) {
2372
+ const cardTags = card.tags ?? [];
2373
+ let maxTargetBoost = 1;
2374
+ const boostedTargets = [];
2375
+ for (const tag of cardTags) {
2376
+ const boost = targetBoosts.get(tag);
2377
+ if (boost && boost > maxTargetBoost) {
2378
+ maxTargetBoost = boost;
2379
+ boostedTargets.push(tag);
2380
+ }
2381
+ }
2382
+ if (maxTargetBoost > 1) {
2383
+ finalScore *= maxTargetBoost;
2384
+ action = "boosted";
2385
+ finalReason = `${finalReason} | targetBoost \xD7${maxTargetBoost.toFixed(2)} for ${boostedTargets.join(", ")}`;
2386
+ logger.info(
2387
+ `[HierarchyDefinition] targetBoost \xD7${maxTargetBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedTargets.join(", ")}] (score: ${card.score.toFixed(3)} \u2192 ${finalScore.toFixed(3)})`
2388
+ );
2389
+ }
2390
+ }
1936
2391
  gated.push({
1937
2392
  ...card,
1938
2393
  score: finalScore,
@@ -2118,12 +2573,12 @@ __export(interferenceMitigator_exports, {
2118
2573
  default: () => InterferenceMitigatorNavigator
2119
2574
  });
2120
2575
  import { toCourseElo as toCourseElo4 } from "@vue-skuilder/common";
2121
- var DEFAULT_MIN_COUNT2, DEFAULT_MIN_ELAPSED_DAYS, DEFAULT_INTERFERENCE_DECAY, InterferenceMitigatorNavigator;
2576
+ var DEFAULT_MIN_COUNT3, DEFAULT_MIN_ELAPSED_DAYS, DEFAULT_INTERFERENCE_DECAY, InterferenceMitigatorNavigator;
2122
2577
  var init_interferenceMitigator = __esm({
2123
2578
  "src/core/navigators/filters/interferenceMitigator.ts"() {
2124
2579
  "use strict";
2125
2580
  init_navigators();
2126
- DEFAULT_MIN_COUNT2 = 10;
2581
+ DEFAULT_MIN_COUNT3 = 10;
2127
2582
  DEFAULT_MIN_ELAPSED_DAYS = 3;
2128
2583
  DEFAULT_INTERFERENCE_DECAY = 0.8;
2129
2584
  InterferenceMitigatorNavigator = class extends ContentNavigator {
@@ -2148,7 +2603,7 @@ var init_interferenceMitigator = __esm({
2148
2603
  return {
2149
2604
  interferenceSets: sets,
2150
2605
  maturityThreshold: {
2151
- minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT2,
2606
+ minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3,
2152
2607
  minElo: parsed.maturityThreshold?.minElo,
2153
2608
  minElapsedDays: parsed.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS
2154
2609
  },
@@ -2158,7 +2613,7 @@ var init_interferenceMitigator = __esm({
2158
2613
  return {
2159
2614
  interferenceSets: [],
2160
2615
  maturityThreshold: {
2161
- minCount: DEFAULT_MIN_COUNT2,
2616
+ minCount: DEFAULT_MIN_COUNT3,
2162
2617
  minElapsedDays: DEFAULT_MIN_ELAPSED_DAYS
2163
2618
  },
2164
2619
  defaultDecay: DEFAULT_INTERFERENCE_DECAY
@@ -2205,7 +2660,7 @@ var init_interferenceMitigator = __esm({
2205
2660
  try {
2206
2661
  const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
2207
2662
  const userElo = toCourseElo4(courseReg.elo);
2208
- const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT2;
2663
+ const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3;
2209
2664
  const minElo = this.config.maturityThreshold?.minElo;
2210
2665
  const minElapsedDays = this.config.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS;
2211
2666
  const minCountForElapsed = minElapsedDays * 2;
@@ -2716,160 +3171,1718 @@ var init_learning = __esm({
2716
3171
  }
2717
3172
  });
2718
3173
 
2719
- // src/core/orchestration/signal.ts
2720
- function computeOutcomeSignal(records, config = {}) {
2721
- if (!records || records.length === 0) {
2722
- return null;
3174
+ // src/core/orchestration/signal.ts
3175
+ function computeOutcomeSignal(records, config = {}) {
3176
+ if (!records || records.length === 0) {
3177
+ return null;
3178
+ }
3179
+ const target = config.targetAccuracy ?? 0.85;
3180
+ const tolerance = config.tolerance ?? 0.05;
3181
+ let correct = 0;
3182
+ for (const r of records) {
3183
+ if (r.isCorrect) correct++;
3184
+ }
3185
+ const accuracy = correct / records.length;
3186
+ return scoreAccuracyInZone(accuracy, target, tolerance);
3187
+ }
3188
+ function scoreAccuracyInZone(accuracy, target, tolerance) {
3189
+ const dist = Math.abs(accuracy - target);
3190
+ if (dist <= tolerance) {
3191
+ return 1;
3192
+ }
3193
+ const excess = dist - tolerance;
3194
+ const slope = 2.5;
3195
+ return Math.max(0, 1 - excess * slope);
3196
+ }
3197
+ var init_signal = __esm({
3198
+ "src/core/orchestration/signal.ts"() {
3199
+ "use strict";
3200
+ }
3201
+ });
3202
+
3203
+ // src/core/orchestration/recording.ts
3204
+ async function recordUserOutcome(context, periodStart, periodEnd, records, activeStrategyIds, eloStart = 0, eloEnd = 0, config) {
3205
+ const { user, course, userId } = context;
3206
+ const courseId = course.getCourseID();
3207
+ const outcomeValue = computeOutcomeSignal(records, config);
3208
+ if (outcomeValue === null) {
3209
+ logger.debug(
3210
+ `[Orchestration] No outcome signal computed for ${userId} (insufficient data). Skipping record.`
3211
+ );
3212
+ return;
3213
+ }
3214
+ const deviations = {};
3215
+ for (const strategyId of activeStrategyIds) {
3216
+ deviations[strategyId] = context.getDeviation(strategyId);
3217
+ }
3218
+ const id = `USER_OUTCOME::${courseId}::${userId}::${periodEnd}`;
3219
+ const record = {
3220
+ _id: id,
3221
+ docType: "USER_OUTCOME" /* USER_OUTCOME */,
3222
+ courseId,
3223
+ userId,
3224
+ periodStart,
3225
+ periodEnd,
3226
+ outcomeValue,
3227
+ deviations,
3228
+ metadata: {
3229
+ sessionsCount: 1,
3230
+ // Assumes recording is triggered per-session currently
3231
+ cardsSeen: records.length,
3232
+ eloStart,
3233
+ eloEnd,
3234
+ signalType: "accuracy_in_zone"
3235
+ }
3236
+ };
3237
+ try {
3238
+ await user.putUserOutcome(record);
3239
+ logger.debug(
3240
+ `[Orchestration] Recorded outcome ${outcomeValue.toFixed(3)} for ${userId} (doc: ${id})`
3241
+ );
3242
+ } catch (e) {
3243
+ logger.error(`[Orchestration] Failed to record outcome: ${e}`);
3244
+ }
3245
+ }
3246
+ var init_recording = __esm({
3247
+ "src/core/orchestration/recording.ts"() {
3248
+ "use strict";
3249
+ init_signal();
3250
+ init_types_legacy();
3251
+ init_logger();
3252
+ }
3253
+ });
3254
+
3255
+ // src/core/orchestration/index.ts
3256
+ function fnv1a(str) {
3257
+ let hash = 2166136261;
3258
+ for (let i = 0; i < str.length; i++) {
3259
+ hash ^= str.charCodeAt(i);
3260
+ hash = Math.imul(hash, 16777619);
3261
+ }
3262
+ return hash >>> 0;
3263
+ }
3264
+ function computeDeviation(userId, strategyId, salt) {
3265
+ const input = `${userId}:${strategyId}:${salt}`;
3266
+ const hash = fnv1a(input);
3267
+ const normalized = hash / 4294967296;
3268
+ return normalized * 2 - 1;
3269
+ }
3270
+ function computeSpread(confidence) {
3271
+ const clampedConfidence = Math.max(0, Math.min(1, confidence));
3272
+ return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
3273
+ }
3274
+ function computeEffectiveWeight(learnable, userId, strategyId, salt) {
3275
+ const deviation = computeDeviation(userId, strategyId, salt);
3276
+ const spread = computeSpread(learnable.confidence);
3277
+ const adjustment = deviation * spread * learnable.weight;
3278
+ const effective = learnable.weight + adjustment;
3279
+ return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
3280
+ }
3281
+ async function createOrchestrationContext(user, course) {
3282
+ let courseConfig;
3283
+ try {
3284
+ courseConfig = await course.getCourseConfig();
3285
+ } catch (e) {
3286
+ logger.error(`[Orchestration] Failed to load course config: ${e}`);
3287
+ courseConfig = {
3288
+ name: "Unknown",
3289
+ description: "",
3290
+ public: false,
3291
+ deleted: false,
3292
+ creator: "",
3293
+ admins: [],
3294
+ moderators: [],
3295
+ dataShapes: [],
3296
+ questionTypes: [],
3297
+ orchestration: { salt: "default" }
3298
+ };
3299
+ }
3300
+ const userId = user.getUsername();
3301
+ const salt = courseConfig.orchestration?.salt || "default_salt";
3302
+ return {
3303
+ user,
3304
+ course,
3305
+ userId,
3306
+ courseConfig,
3307
+ getEffectiveWeight(strategyId, learnable) {
3308
+ return computeEffectiveWeight(learnable, userId, strategyId, salt);
3309
+ },
3310
+ getDeviation(strategyId) {
3311
+ return computeDeviation(userId, strategyId, salt);
3312
+ }
3313
+ };
3314
+ }
3315
+ var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
3316
+ var init_orchestration = __esm({
3317
+ "src/core/orchestration/index.ts"() {
3318
+ "use strict";
3319
+ init_logger();
3320
+ init_gradient();
3321
+ init_learning();
3322
+ init_signal();
3323
+ init_recording();
3324
+ MIN_SPREAD = 0.1;
3325
+ MAX_SPREAD = 0.5;
3326
+ MIN_WEIGHT = 0.1;
3327
+ MAX_WEIGHT = 3;
3328
+ }
3329
+ });
3330
+
3331
+ // src/study/SpacedRepetition.ts
3332
+ import moment4 from "moment";
3333
+ import { isTaggedPerformance } from "@vue-skuilder/common";
3334
+ var duration;
3335
+ var init_SpacedRepetition = __esm({
3336
+ "src/study/SpacedRepetition.ts"() {
3337
+ "use strict";
3338
+ init_util();
3339
+ init_logger();
3340
+ duration = moment4.duration;
3341
+ }
3342
+ });
3343
+
3344
+ // src/study/services/SrsService.ts
3345
+ import moment5 from "moment";
3346
+ var init_SrsService = __esm({
3347
+ "src/study/services/SrsService.ts"() {
3348
+ "use strict";
3349
+ init_couch();
3350
+ init_SpacedRepetition();
3351
+ init_logger();
3352
+ }
3353
+ });
3354
+
3355
+ // src/study/services/EloService.ts
3356
+ import {
3357
+ adjustCourseScores,
3358
+ adjustCourseScoresPerTag,
3359
+ toCourseElo as toCourseElo5
3360
+ } from "@vue-skuilder/common";
3361
+ var init_EloService = __esm({
3362
+ "src/study/services/EloService.ts"() {
3363
+ "use strict";
3364
+ init_logger();
3365
+ }
3366
+ });
3367
+
3368
+ // src/study/services/ResponseProcessor.ts
3369
+ import { isTaggedPerformance as isTaggedPerformance2 } from "@vue-skuilder/common";
3370
+ var init_ResponseProcessor = __esm({
3371
+ "src/study/services/ResponseProcessor.ts"() {
3372
+ "use strict";
3373
+ init_core();
3374
+ init_logger();
3375
+ }
3376
+ });
3377
+
3378
+ // src/study/services/CardHydrationService.ts
3379
+ import {
3380
+ displayableDataToViewData,
3381
+ isCourseElo,
3382
+ toCourseElo as toCourseElo6
3383
+ } from "@vue-skuilder/common";
3384
+ var init_CardHydrationService = __esm({
3385
+ "src/study/services/CardHydrationService.ts"() {
3386
+ "use strict";
3387
+ init_logger();
3388
+ }
3389
+ });
3390
+
3391
+ // src/study/ItemQueue.ts
3392
+ var init_ItemQueue = __esm({
3393
+ "src/study/ItemQueue.ts"() {
3394
+ "use strict";
3395
+ }
3396
+ });
3397
+
3398
+ // src/util/packer/types.ts
3399
+ var init_types3 = __esm({
3400
+ "src/util/packer/types.ts"() {
3401
+ "use strict";
3402
+ }
3403
+ });
3404
+
3405
+ // src/util/packer/CouchDBToStaticPacker.ts
3406
+ var init_CouchDBToStaticPacker = __esm({
3407
+ "src/util/packer/CouchDBToStaticPacker.ts"() {
3408
+ "use strict";
3409
+ init_types_legacy();
3410
+ init_logger();
3411
+ }
3412
+ });
3413
+
3414
+ // src/util/packer/index.ts
3415
+ var init_packer = __esm({
3416
+ "src/util/packer/index.ts"() {
3417
+ "use strict";
3418
+ init_types3();
3419
+ init_CouchDBToStaticPacker();
3420
+ }
3421
+ });
3422
+
3423
+ // src/util/migrator/types.ts
3424
+ var DEFAULT_MIGRATION_OPTIONS;
3425
+ var init_types4 = __esm({
3426
+ "src/util/migrator/types.ts"() {
3427
+ "use strict";
3428
+ DEFAULT_MIGRATION_OPTIONS = {
3429
+ chunkBatchSize: 100,
3430
+ validateRoundTrip: false,
3431
+ cleanupOnFailure: true,
3432
+ timeout: 3e5
3433
+ // 5 minutes
3434
+ };
3435
+ }
3436
+ });
3437
+
3438
+ // src/util/migrator/FileSystemAdapter.ts
3439
+ var FileSystemError;
3440
+ var init_FileSystemAdapter = __esm({
3441
+ "src/util/migrator/FileSystemAdapter.ts"() {
3442
+ "use strict";
3443
+ FileSystemError = class extends Error {
3444
+ constructor(message, operation, filePath, cause) {
3445
+ super(message);
3446
+ this.operation = operation;
3447
+ this.filePath = filePath;
3448
+ this.cause = cause;
3449
+ this.name = "FileSystemError";
3450
+ }
3451
+ };
3452
+ }
3453
+ });
3454
+
3455
+ // src/util/migrator/validation.ts
3456
+ async function validateStaticCourse(staticPath, fs) {
3457
+ const validation = {
3458
+ valid: true,
3459
+ manifestExists: false,
3460
+ chunksExist: false,
3461
+ attachmentsExist: false,
3462
+ errors: [],
3463
+ warnings: []
3464
+ };
3465
+ try {
3466
+ if (fs) {
3467
+ const stats = await fs.stat(staticPath);
3468
+ if (!stats.isDirectory()) {
3469
+ validation.errors.push(`Path is not a directory: ${staticPath}`);
3470
+ validation.valid = false;
3471
+ return validation;
3472
+ }
3473
+ } else if (!nodeFS) {
3474
+ validation.errors.push("File system access not available - validation skipped");
3475
+ validation.valid = false;
3476
+ return validation;
3477
+ } else {
3478
+ const stats = await nodeFS.promises.stat(staticPath);
3479
+ if (!stats.isDirectory()) {
3480
+ validation.errors.push(`Path is not a directory: ${staticPath}`);
3481
+ validation.valid = false;
3482
+ return validation;
3483
+ }
3484
+ }
3485
+ let manifestPath = `${staticPath}/manifest.json`;
3486
+ try {
3487
+ if (fs) {
3488
+ manifestPath = fs.joinPath(staticPath, "manifest.json");
3489
+ if (await fs.exists(manifestPath)) {
3490
+ validation.manifestExists = true;
3491
+ const manifestContent = await fs.readFile(manifestPath);
3492
+ const manifest = JSON.parse(manifestContent);
3493
+ validation.courseId = manifest.courseId;
3494
+ validation.courseName = manifest.courseName;
3495
+ if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
3496
+ validation.errors.push("Invalid manifest structure");
3497
+ validation.valid = false;
3498
+ }
3499
+ } else {
3500
+ validation.errors.push(`Manifest not found: ${manifestPath}`);
3501
+ validation.valid = false;
3502
+ }
3503
+ } else {
3504
+ manifestPath = `${staticPath}/manifest.json`;
3505
+ await nodeFS.promises.access(manifestPath);
3506
+ validation.manifestExists = true;
3507
+ const manifestContent = await nodeFS.promises.readFile(manifestPath, "utf8");
3508
+ const manifest = JSON.parse(manifestContent);
3509
+ validation.courseId = manifest.courseId;
3510
+ validation.courseName = manifest.courseName;
3511
+ if (!manifest.version || !manifest.courseId || !manifest.chunks || !Array.isArray(manifest.chunks)) {
3512
+ validation.errors.push("Invalid manifest structure");
3513
+ validation.valid = false;
3514
+ }
3515
+ }
3516
+ } catch (error) {
3517
+ const errorMessage = error instanceof FileSystemError ? error.message : `Manifest not found or invalid: ${manifestPath}`;
3518
+ validation.errors.push(errorMessage);
3519
+ validation.valid = false;
3520
+ }
3521
+ let chunksPath = `${staticPath}/chunks`;
3522
+ try {
3523
+ if (fs) {
3524
+ chunksPath = fs.joinPath(staticPath, "chunks");
3525
+ if (await fs.exists(chunksPath)) {
3526
+ const chunksStats = await fs.stat(chunksPath);
3527
+ if (chunksStats.isDirectory()) {
3528
+ validation.chunksExist = true;
3529
+ } else {
3530
+ validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
3531
+ validation.valid = false;
3532
+ }
3533
+ } else {
3534
+ validation.errors.push(`Chunks directory not found: ${chunksPath}`);
3535
+ validation.valid = false;
3536
+ }
3537
+ } else {
3538
+ chunksPath = `${staticPath}/chunks`;
3539
+ const chunksStats = await nodeFS.promises.stat(chunksPath);
3540
+ if (chunksStats.isDirectory()) {
3541
+ validation.chunksExist = true;
3542
+ } else {
3543
+ validation.errors.push(`Chunks path is not a directory: ${chunksPath}`);
3544
+ validation.valid = false;
3545
+ }
3546
+ }
3547
+ } catch (error) {
3548
+ const errorMessage = error instanceof FileSystemError ? error.message : `Chunks directory not found: ${chunksPath}`;
3549
+ validation.errors.push(errorMessage);
3550
+ validation.valid = false;
3551
+ }
3552
+ let attachmentsPath;
3553
+ try {
3554
+ if (fs) {
3555
+ attachmentsPath = fs.joinPath(staticPath, "attachments");
3556
+ if (await fs.exists(attachmentsPath)) {
3557
+ const attachmentsStats = await fs.stat(attachmentsPath);
3558
+ if (attachmentsStats.isDirectory()) {
3559
+ validation.attachmentsExist = true;
3560
+ }
3561
+ } else {
3562
+ validation.warnings.push(
3563
+ `Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`
3564
+ );
3565
+ }
3566
+ } else {
3567
+ attachmentsPath = `${staticPath}/attachments`;
3568
+ const attachmentsStats = await nodeFS.promises.stat(attachmentsPath);
3569
+ if (attachmentsStats.isDirectory()) {
3570
+ validation.attachmentsExist = true;
3571
+ }
3572
+ }
3573
+ } catch (error) {
3574
+ attachmentsPath = attachmentsPath || `${staticPath}/attachments`;
3575
+ const warningMessage = error instanceof FileSystemError ? error.message : `Attachments directory not found: ${attachmentsPath} (this is OK if course has no attachments)`;
3576
+ validation.warnings.push(warningMessage);
3577
+ }
3578
+ } catch (error) {
3579
+ validation.errors.push(
3580
+ `Failed to validate static course: ${error instanceof Error ? error.message : String(error)}`
3581
+ );
3582
+ validation.valid = false;
3583
+ }
3584
+ return validation;
3585
+ }
3586
+ async function validateMigration(targetDB, expectedCounts, manifest) {
3587
+ const validation = {
3588
+ valid: true,
3589
+ documentCountMatch: false,
3590
+ attachmentIntegrity: false,
3591
+ viewFunctionality: false,
3592
+ issues: []
3593
+ };
3594
+ try {
3595
+ logger.info("Starting migration validation...");
3596
+ const actualCounts = await getActualDocumentCounts(targetDB);
3597
+ validation.documentCountMatch = compareDocumentCounts(
3598
+ expectedCounts,
3599
+ actualCounts,
3600
+ validation.issues
3601
+ );
3602
+ await validateCourseConfig(targetDB, manifest, validation.issues);
3603
+ validation.viewFunctionality = await validateViews(targetDB, manifest, validation.issues);
3604
+ validation.attachmentIntegrity = await validateAttachmentIntegrity(targetDB, validation.issues);
3605
+ validation.valid = validation.documentCountMatch && validation.viewFunctionality && validation.attachmentIntegrity;
3606
+ logger.info(`Migration validation completed. Valid: ${validation.valid}`);
3607
+ if (validation.issues.length > 0) {
3608
+ logger.info(`Validation issues: ${validation.issues.length}`);
3609
+ validation.issues.forEach((issue) => {
3610
+ if (issue.type === "error") {
3611
+ logger.error(`${issue.category}: ${issue.message}`);
3612
+ } else {
3613
+ logger.warn(`${issue.category}: ${issue.message}`);
3614
+ }
3615
+ });
3616
+ }
3617
+ } catch (error) {
3618
+ validation.valid = false;
3619
+ validation.issues.push({
3620
+ type: "error",
3621
+ category: "metadata",
3622
+ message: `Validation failed: ${error instanceof Error ? error.message : String(error)}`
3623
+ });
3624
+ }
3625
+ return validation;
3626
+ }
3627
+ async function getActualDocumentCounts(db) {
3628
+ const counts = {};
3629
+ try {
3630
+ const allDocs = await db.allDocs({ include_docs: true });
3631
+ for (const row of allDocs.rows) {
3632
+ if (row.id.startsWith("_design/")) {
3633
+ counts["_design"] = (counts["_design"] || 0) + 1;
3634
+ continue;
3635
+ }
3636
+ const doc = row.doc;
3637
+ if (doc && doc.docType) {
3638
+ counts[doc.docType] = (counts[doc.docType] || 0) + 1;
3639
+ } else {
3640
+ counts["unknown"] = (counts["unknown"] || 0) + 1;
3641
+ }
3642
+ }
3643
+ } catch (error) {
3644
+ logger.error("Failed to get actual document counts:", error);
3645
+ }
3646
+ return counts;
3647
+ }
3648
+ function compareDocumentCounts(expected, actual, issues) {
3649
+ let countsMatch = true;
3650
+ for (const [docType, expectedCount] of Object.entries(expected)) {
3651
+ const actualCount = actual[docType] || 0;
3652
+ if (actualCount !== expectedCount) {
3653
+ countsMatch = false;
3654
+ issues.push({
3655
+ type: "error",
3656
+ category: "documents",
3657
+ message: `Document count mismatch for ${docType}: expected ${expectedCount}, got ${actualCount}`
3658
+ });
3659
+ }
3660
+ }
3661
+ for (const [docType, actualCount] of Object.entries(actual)) {
3662
+ if (!expected[docType] && docType !== "_design") {
3663
+ issues.push({
3664
+ type: "warning",
3665
+ category: "documents",
3666
+ message: `Unexpected document type found: ${docType} (${actualCount} documents)`
3667
+ });
3668
+ }
3669
+ }
3670
+ return countsMatch;
3671
+ }
3672
+ async function validateCourseConfig(db, manifest, issues) {
3673
+ try {
3674
+ const courseConfig = await db.get("CourseConfig");
3675
+ if (!courseConfig) {
3676
+ issues.push({
3677
+ type: "error",
3678
+ category: "course_config",
3679
+ message: "CourseConfig document not found after migration"
3680
+ });
3681
+ return;
3682
+ }
3683
+ if (!courseConfig.courseID) {
3684
+ issues.push({
3685
+ type: "warning",
3686
+ category: "course_config",
3687
+ message: "CourseConfig document missing courseID field"
3688
+ });
3689
+ }
3690
+ if (courseConfig.courseID !== manifest.courseId) {
3691
+ issues.push({
3692
+ type: "warning",
3693
+ category: "course_config",
3694
+ message: `CourseConfig courseID mismatch: expected ${manifest.courseId}, got ${courseConfig.courseID}`
3695
+ });
3696
+ }
3697
+ logger.debug("CourseConfig document validation passed");
3698
+ } catch (error) {
3699
+ if (error.status === 404) {
3700
+ issues.push({
3701
+ type: "error",
3702
+ category: "course_config",
3703
+ message: "CourseConfig document not found in database"
3704
+ });
3705
+ } else {
3706
+ issues.push({
3707
+ type: "error",
3708
+ category: "course_config",
3709
+ message: `Failed to validate CourseConfig document: ${error instanceof Error ? error.message : String(error)}`
3710
+ });
3711
+ }
3712
+ }
3713
+ }
3714
+ async function validateViews(db, manifest, issues) {
3715
+ let viewsValid = true;
3716
+ try {
3717
+ for (const designDoc of manifest.designDocs) {
3718
+ try {
3719
+ const doc = await db.get(designDoc._id);
3720
+ if (!doc) {
3721
+ viewsValid = false;
3722
+ issues.push({
3723
+ type: "error",
3724
+ category: "views",
3725
+ message: `Design document not found: ${designDoc._id}`
3726
+ });
3727
+ continue;
3728
+ }
3729
+ for (const viewName of Object.keys(designDoc.views)) {
3730
+ try {
3731
+ const viewPath = `${designDoc._id}/${viewName}`;
3732
+ await db.query(viewPath, { limit: 1 });
3733
+ } catch (viewError) {
3734
+ viewsValid = false;
3735
+ issues.push({
3736
+ type: "error",
3737
+ category: "views",
3738
+ message: `View not accessible: ${designDoc._id}/${viewName} - ${viewError}`
3739
+ });
3740
+ }
3741
+ }
3742
+ } catch (error) {
3743
+ viewsValid = false;
3744
+ issues.push({
3745
+ type: "error",
3746
+ category: "views",
3747
+ message: `Failed to validate design document ${designDoc._id}: ${error}`
3748
+ });
3749
+ }
3750
+ }
3751
+ } catch (error) {
3752
+ viewsValid = false;
3753
+ issues.push({
3754
+ type: "error",
3755
+ category: "views",
3756
+ message: `View validation failed: ${error instanceof Error ? error.message : String(error)}`
3757
+ });
3758
+ }
3759
+ return viewsValid;
3760
+ }
3761
+ async function validateAttachmentIntegrity(db, issues) {
3762
+ let attachmentsValid = true;
3763
+ try {
3764
+ const allDocs = await db.allDocs({
3765
+ include_docs: true,
3766
+ limit: 10
3767
+ // Sample first 10 documents for performance
3768
+ });
3769
+ let attachmentCount = 0;
3770
+ let validAttachments = 0;
3771
+ for (const row of allDocs.rows) {
3772
+ const doc = row.doc;
3773
+ if (doc && doc._attachments) {
3774
+ for (const [attachmentName, _attachmentMeta] of Object.entries(doc._attachments)) {
3775
+ attachmentCount++;
3776
+ try {
3777
+ const attachment = await db.getAttachment(doc._id, attachmentName);
3778
+ if (attachment) {
3779
+ validAttachments++;
3780
+ }
3781
+ } catch (attachmentError) {
3782
+ attachmentsValid = false;
3783
+ issues.push({
3784
+ type: "error",
3785
+ category: "attachments",
3786
+ message: `Attachment not accessible: ${doc._id}/${attachmentName} - ${attachmentError}`
3787
+ });
3788
+ }
3789
+ }
3790
+ }
3791
+ }
3792
+ if (attachmentCount === 0) {
3793
+ issues.push({
3794
+ type: "warning",
3795
+ category: "attachments",
3796
+ message: "No attachments found in sampled documents"
3797
+ });
3798
+ } else {
3799
+ logger.info(`Validated ${validAttachments}/${attachmentCount} sampled attachments`);
3800
+ }
3801
+ } catch (error) {
3802
+ attachmentsValid = false;
3803
+ issues.push({
3804
+ type: "error",
3805
+ category: "attachments",
3806
+ message: `Attachment validation failed: ${error instanceof Error ? error.message : String(error)}`
3807
+ });
3808
+ }
3809
+ return attachmentsValid;
3810
+ }
3811
+ var nodeFS;
3812
+ var init_validation = __esm({
3813
+ "src/util/migrator/validation.ts"() {
3814
+ "use strict";
3815
+ init_logger();
3816
+ init_FileSystemAdapter();
3817
+ nodeFS = null;
3818
+ try {
3819
+ if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
3820
+ nodeFS = eval("require")("fs");
3821
+ nodeFS.promises = nodeFS.promises || eval("require")("fs").promises;
3822
+ }
3823
+ } catch {
3824
+ }
3825
+ }
3826
+ });
3827
+
3828
+ // src/util/migrator/StaticToCouchDBMigrator.ts
3829
+ var nodeFS2, nodePath, StaticToCouchDBMigrator;
3830
+ var init_StaticToCouchDBMigrator = __esm({
3831
+ "src/util/migrator/StaticToCouchDBMigrator.ts"() {
3832
+ "use strict";
3833
+ init_logger();
3834
+ init_types4();
3835
+ init_validation();
3836
+ init_FileSystemAdapter();
3837
+ nodeFS2 = null;
3838
+ nodePath = null;
3839
+ try {
3840
+ if (typeof window === "undefined" && typeof process !== "undefined" && process.versions?.node) {
3841
+ nodeFS2 = eval("require")("fs");
3842
+ nodePath = eval("require")("path");
3843
+ nodeFS2.promises = nodeFS2.promises || eval("require")("fs").promises;
3844
+ }
3845
+ } catch {
3846
+ }
3847
+ StaticToCouchDBMigrator = class {
3848
+ options;
3849
+ progressCallback;
3850
+ fs;
3851
+ constructor(options = {}, fileSystemAdapter) {
3852
+ this.options = {
3853
+ ...DEFAULT_MIGRATION_OPTIONS,
3854
+ ...options
3855
+ };
3856
+ this.fs = fileSystemAdapter;
3857
+ }
3858
+ /**
3859
+ * Set a progress callback to receive updates during migration
3860
+ */
3861
+ setProgressCallback(callback) {
3862
+ this.progressCallback = callback;
3863
+ }
3864
+ /**
3865
+ * Migrate a static course to CouchDB
3866
+ */
3867
+ async migrateCourse(staticPath, targetDB) {
3868
+ const startTime = Date.now();
3869
+ const result = {
3870
+ success: false,
3871
+ documentsRestored: 0,
3872
+ attachmentsRestored: 0,
3873
+ designDocsRestored: 0,
3874
+ courseConfigRestored: 0,
3875
+ errors: [],
3876
+ warnings: [],
3877
+ migrationTime: 0
3878
+ };
3879
+ try {
3880
+ logger.info(`Starting migration from ${staticPath} to CouchDB`);
3881
+ this.reportProgress("manifest", 0, 1, "Validating static course...");
3882
+ const validation = await validateStaticCourse(staticPath, this.fs);
3883
+ if (!validation.valid) {
3884
+ result.errors.push(...validation.errors);
3885
+ throw new Error(`Static course validation failed: ${validation.errors.join(", ")}`);
3886
+ }
3887
+ result.warnings.push(...validation.warnings);
3888
+ this.reportProgress("manifest", 1, 1, "Loading course manifest...");
3889
+ const manifest = await this.loadManifest(staticPath);
3890
+ logger.info(`Loaded manifest for course: ${manifest.courseId} (${manifest.courseName})`);
3891
+ this.reportProgress(
3892
+ "design_docs",
3893
+ 0,
3894
+ manifest.designDocs.length,
3895
+ "Restoring design documents..."
3896
+ );
3897
+ const designDocResults = await this.restoreDesignDocuments(manifest.designDocs, targetDB);
3898
+ result.designDocsRestored = designDocResults.restored;
3899
+ result.errors.push(...designDocResults.errors);
3900
+ result.warnings.push(...designDocResults.warnings);
3901
+ this.reportProgress("course_config", 0, 1, "Restoring CourseConfig document...");
3902
+ const courseConfigResults = await this.restoreCourseConfig(manifest, targetDB);
3903
+ result.courseConfigRestored = courseConfigResults.restored;
3904
+ result.errors.push(...courseConfigResults.errors);
3905
+ result.warnings.push(...courseConfigResults.warnings);
3906
+ this.reportProgress("course_config", 1, 1, "CourseConfig document restored");
3907
+ const expectedCounts = this.calculateExpectedCounts(manifest);
3908
+ this.reportProgress(
3909
+ "documents",
3910
+ 0,
3911
+ manifest.documentCount,
3912
+ "Aggregating documents from chunks..."
3913
+ );
3914
+ const documents = await this.aggregateDocuments(staticPath, manifest);
3915
+ const filteredDocuments = documents.filter((doc) => doc._id !== "CourseConfig");
3916
+ if (documents.length !== filteredDocuments.length) {
3917
+ result.warnings.push(
3918
+ `Filtered out ${documents.length - filteredDocuments.length} CourseConfig document(s) from chunks to prevent conflicts`
3919
+ );
3920
+ }
3921
+ this.reportProgress(
3922
+ "documents",
3923
+ filteredDocuments.length,
3924
+ manifest.documentCount,
3925
+ "Uploading documents to CouchDB..."
3926
+ );
3927
+ const docResults = await this.uploadDocuments(filteredDocuments, targetDB);
3928
+ result.documentsRestored = docResults.restored;
3929
+ result.errors.push(...docResults.errors);
3930
+ result.warnings.push(...docResults.warnings);
3931
+ const docsWithAttachments = documents.filter(
3932
+ (doc) => doc._attachments && Object.keys(doc._attachments).length > 0
3933
+ );
3934
+ this.reportProgress("attachments", 0, docsWithAttachments.length, "Uploading attachments...");
3935
+ const attachmentResults = await this.uploadAttachments(
3936
+ staticPath,
3937
+ docsWithAttachments,
3938
+ targetDB
3939
+ );
3940
+ result.attachmentsRestored = attachmentResults.restored;
3941
+ result.errors.push(...attachmentResults.errors);
3942
+ result.warnings.push(...attachmentResults.warnings);
3943
+ if (this.options.validateRoundTrip) {
3944
+ this.reportProgress("validation", 0, 1, "Validating migration...");
3945
+ const validationResult = await validateMigration(targetDB, expectedCounts, manifest);
3946
+ if (!validationResult.valid) {
3947
+ result.warnings.push("Migration validation found issues");
3948
+ validationResult.issues.forEach((issue) => {
3949
+ if (issue.type === "error") {
3950
+ result.errors.push(`Validation: ${issue.message}`);
3951
+ } else {
3952
+ result.warnings.push(`Validation: ${issue.message}`);
3953
+ }
3954
+ });
3955
+ }
3956
+ this.reportProgress("validation", 1, 1, "Migration validation completed");
3957
+ }
3958
+ result.success = result.errors.length === 0;
3959
+ result.migrationTime = Date.now() - startTime;
3960
+ logger.info(`Migration completed in ${result.migrationTime}ms`);
3961
+ logger.info(`Documents restored: ${result.documentsRestored}`);
3962
+ logger.info(`Attachments restored: ${result.attachmentsRestored}`);
3963
+ logger.info(`Design docs restored: ${result.designDocsRestored}`);
3964
+ logger.info(`CourseConfig restored: ${result.courseConfigRestored}`);
3965
+ if (result.errors.length > 0) {
3966
+ logger.error(`Migration completed with ${result.errors.length} errors`);
3967
+ }
3968
+ if (result.warnings.length > 0) {
3969
+ logger.warn(`Migration completed with ${result.warnings.length} warnings`);
3970
+ }
3971
+ } catch (error) {
3972
+ result.success = false;
3973
+ result.migrationTime = Date.now() - startTime;
3974
+ const errorMessage = error instanceof Error ? error.message : String(error);
3975
+ result.errors.push(`Migration failed: ${errorMessage}`);
3976
+ logger.error("Migration failed:", error);
3977
+ if (this.options.cleanupOnFailure) {
3978
+ try {
3979
+ await this.cleanupFailedMigration(targetDB);
3980
+ } catch (cleanupError) {
3981
+ logger.error("Failed to cleanup after migration failure:", cleanupError);
3982
+ result.warnings.push("Failed to cleanup after migration failure");
3983
+ }
3984
+ }
3985
+ }
3986
+ return result;
3987
+ }
3988
+ /**
3989
+ * Load and parse the manifest file
3990
+ */
3991
+ async loadManifest(staticPath) {
3992
+ try {
3993
+ let manifestContent;
3994
+ let manifestPath;
3995
+ if (this.fs) {
3996
+ manifestPath = this.fs.joinPath(staticPath, "manifest.json");
3997
+ manifestContent = await this.fs.readFile(manifestPath);
3998
+ } else {
3999
+ manifestPath = nodeFS2 && nodePath ? nodePath.join(staticPath, "manifest.json") : `${staticPath}/manifest.json`;
4000
+ if (nodeFS2 && this.isLocalPath(staticPath)) {
4001
+ manifestContent = await nodeFS2.promises.readFile(manifestPath, "utf8");
4002
+ } else {
4003
+ const response = await fetch(manifestPath);
4004
+ if (!response.ok) {
4005
+ throw new Error(`Failed to fetch manifest: ${response.status} ${response.statusText}`);
4006
+ }
4007
+ manifestContent = await response.text();
4008
+ }
4009
+ }
4010
+ const manifest = JSON.parse(manifestContent);
4011
+ if (!manifest.version || !manifest.courseId || !manifest.chunks) {
4012
+ throw new Error("Invalid manifest structure");
4013
+ }
4014
+ return manifest;
4015
+ } catch (error) {
4016
+ const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load manifest: ${error instanceof Error ? error.message : String(error)}`;
4017
+ throw new Error(errorMessage);
4018
+ }
4019
+ }
4020
+ /**
4021
+ * Restore design documents to CouchDB
4022
+ */
4023
+ async restoreDesignDocuments(designDocs, db) {
4024
+ const result = { restored: 0, errors: [], warnings: [] };
4025
+ for (let i = 0; i < designDocs.length; i++) {
4026
+ const designDoc = designDocs[i];
4027
+ this.reportProgress("design_docs", i, designDocs.length, `Restoring ${designDoc._id}...`);
4028
+ try {
4029
+ let existingDoc;
4030
+ try {
4031
+ existingDoc = await db.get(designDoc._id);
4032
+ } catch {
4033
+ }
4034
+ const docToInsert = {
4035
+ _id: designDoc._id,
4036
+ views: designDoc.views
4037
+ };
4038
+ if (existingDoc) {
4039
+ docToInsert._rev = existingDoc._rev;
4040
+ logger.debug(`Updating existing design document: ${designDoc._id}`);
4041
+ } else {
4042
+ logger.debug(`Creating new design document: ${designDoc._id}`);
4043
+ }
4044
+ await db.put(docToInsert);
4045
+ result.restored++;
4046
+ } catch (error) {
4047
+ const errorMessage = `Failed to restore design document ${designDoc._id}: ${error instanceof Error ? error.message : String(error)}`;
4048
+ result.errors.push(errorMessage);
4049
+ logger.error(errorMessage);
4050
+ }
4051
+ }
4052
+ this.reportProgress(
4053
+ "design_docs",
4054
+ designDocs.length,
4055
+ designDocs.length,
4056
+ `Restored ${result.restored} design documents`
4057
+ );
4058
+ return result;
4059
+ }
4060
+ /**
4061
+ * Aggregate documents from all chunks
4062
+ */
4063
+ async aggregateDocuments(staticPath, manifest) {
4064
+ const allDocuments = [];
4065
+ const documentMap = /* @__PURE__ */ new Map();
4066
+ for (let i = 0; i < manifest.chunks.length; i++) {
4067
+ const chunk = manifest.chunks[i];
4068
+ this.reportProgress(
4069
+ "documents",
4070
+ allDocuments.length,
4071
+ manifest.documentCount,
4072
+ `Loading chunk ${chunk.id}...`
4073
+ );
4074
+ try {
4075
+ const documents = await this.loadChunk(staticPath, chunk);
4076
+ for (const doc of documents) {
4077
+ if (!doc._id) {
4078
+ logger.warn(`Document without _id found in chunk ${chunk.id}, skipping`);
4079
+ continue;
4080
+ }
4081
+ if (documentMap.has(doc._id)) {
4082
+ logger.warn(`Duplicate document ID found: ${doc._id}, using latest version`);
4083
+ }
4084
+ documentMap.set(doc._id, doc);
4085
+ }
4086
+ } catch (error) {
4087
+ throw new Error(
4088
+ `Failed to load chunk ${chunk.id}: ${error instanceof Error ? error.message : String(error)}`
4089
+ );
4090
+ }
4091
+ }
4092
+ allDocuments.push(...documentMap.values());
4093
+ logger.info(
4094
+ `Aggregated ${allDocuments.length} unique documents from ${manifest.chunks.length} chunks`
4095
+ );
4096
+ return allDocuments;
4097
+ }
4098
+ /**
4099
+ * Load documents from a single chunk file
4100
+ */
4101
+ async loadChunk(staticPath, chunk) {
4102
+ try {
4103
+ let chunkContent;
4104
+ let chunkPath;
4105
+ if (this.fs) {
4106
+ chunkPath = this.fs.joinPath(staticPath, chunk.path);
4107
+ chunkContent = await this.fs.readFile(chunkPath);
4108
+ } else {
4109
+ chunkPath = nodeFS2 && nodePath ? nodePath.join(staticPath, chunk.path) : `${staticPath}/${chunk.path}`;
4110
+ if (nodeFS2 && this.isLocalPath(staticPath)) {
4111
+ chunkContent = await nodeFS2.promises.readFile(chunkPath, "utf8");
4112
+ } else {
4113
+ const response = await fetch(chunkPath);
4114
+ if (!response.ok) {
4115
+ throw new Error(`Failed to fetch chunk: ${response.status} ${response.statusText}`);
4116
+ }
4117
+ chunkContent = await response.text();
4118
+ }
4119
+ }
4120
+ const documents = JSON.parse(chunkContent);
4121
+ if (!Array.isArray(documents)) {
4122
+ throw new Error("Chunk file does not contain an array of documents");
4123
+ }
4124
+ return documents;
4125
+ } catch (error) {
4126
+ const errorMessage = error instanceof FileSystemError ? error.message : `Failed to load chunk: ${error instanceof Error ? error.message : String(error)}`;
4127
+ throw new Error(errorMessage);
4128
+ }
4129
+ }
4130
+ /**
4131
+ * Upload documents to CouchDB in batches
4132
+ */
4133
+ async uploadDocuments(documents, db) {
4134
+ const result = { restored: 0, errors: [], warnings: [] };
4135
+ const batchSize = this.options.chunkBatchSize;
4136
+ for (let i = 0; i < documents.length; i += batchSize) {
4137
+ const batch = documents.slice(i, i + batchSize);
4138
+ this.reportProgress(
4139
+ "documents",
4140
+ i,
4141
+ documents.length,
4142
+ `Uploading batch ${Math.floor(i / batchSize) + 1}...`
4143
+ );
4144
+ try {
4145
+ const docsToInsert = batch.map((doc) => {
4146
+ const cleanDoc = { ...doc };
4147
+ delete cleanDoc._rev;
4148
+ delete cleanDoc._attachments;
4149
+ return cleanDoc;
4150
+ });
4151
+ const bulkResult = await db.bulkDocs(docsToInsert);
4152
+ for (let j = 0; j < bulkResult.length; j++) {
4153
+ const docResult = bulkResult[j];
4154
+ const originalDoc = batch[j];
4155
+ if ("error" in docResult) {
4156
+ const errorMessage = `Failed to upload document ${originalDoc._id}: ${docResult.error} - ${docResult.reason}`;
4157
+ result.errors.push(errorMessage);
4158
+ logger.error(errorMessage);
4159
+ } else {
4160
+ result.restored++;
4161
+ }
4162
+ }
4163
+ } catch (error) {
4164
+ let errorMessage;
4165
+ if (error instanceof Error) {
4166
+ errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
4167
+ } else if (error && typeof error === "object" && "message" in error) {
4168
+ errorMessage = `Failed to upload document batch starting at index ${i}: ${error.message}`;
4169
+ } else {
4170
+ errorMessage = `Failed to upload document batch starting at index ${i}: ${JSON.stringify(error)}`;
4171
+ }
4172
+ result.errors.push(errorMessage);
4173
+ logger.error(errorMessage);
4174
+ }
4175
+ }
4176
+ this.reportProgress(
4177
+ "documents",
4178
+ documents.length,
4179
+ documents.length,
4180
+ `Uploaded ${result.restored} documents`
4181
+ );
4182
+ return result;
4183
+ }
4184
+ /**
4185
+ * Upload attachments from filesystem to CouchDB
4186
+ */
4187
+ async uploadAttachments(staticPath, documents, db) {
4188
+ const result = { restored: 0, errors: [], warnings: [] };
4189
+ let processedDocs = 0;
4190
+ for (const doc of documents) {
4191
+ this.reportProgress(
4192
+ "attachments",
4193
+ processedDocs,
4194
+ documents.length,
4195
+ `Processing attachments for ${doc._id}...`
4196
+ );
4197
+ processedDocs++;
4198
+ if (!doc._attachments) {
4199
+ continue;
4200
+ }
4201
+ for (const [attachmentName, attachmentMeta] of Object.entries(doc._attachments)) {
4202
+ try {
4203
+ const uploadResult = await this.uploadSingleAttachment(
4204
+ staticPath,
4205
+ doc._id,
4206
+ attachmentName,
4207
+ attachmentMeta,
4208
+ db
4209
+ );
4210
+ if (uploadResult.success) {
4211
+ result.restored++;
4212
+ } else {
4213
+ result.errors.push(uploadResult.error || "Unknown attachment upload error");
4214
+ }
4215
+ } catch (error) {
4216
+ const errorMessage = `Failed to upload attachment ${doc._id}/${attachmentName}: ${error instanceof Error ? error.message : String(error)}`;
4217
+ result.errors.push(errorMessage);
4218
+ logger.error(errorMessage);
4219
+ }
4220
+ }
4221
+ }
4222
+ this.reportProgress(
4223
+ "attachments",
4224
+ documents.length,
4225
+ documents.length,
4226
+ `Uploaded ${result.restored} attachments`
4227
+ );
4228
+ return result;
4229
+ }
4230
+ /**
4231
+ * Upload a single attachment file
4232
+ */
4233
+ async uploadSingleAttachment(staticPath, docId, attachmentName, attachmentMeta, db) {
4234
+ const result = {
4235
+ success: false,
4236
+ attachmentName,
4237
+ docId
4238
+ };
4239
+ try {
4240
+ if (!attachmentMeta.path) {
4241
+ result.error = "Attachment metadata missing file path";
4242
+ return result;
4243
+ }
4244
+ let attachmentData;
4245
+ let attachmentPath;
4246
+ if (this.fs) {
4247
+ attachmentPath = this.fs.joinPath(staticPath, attachmentMeta.path);
4248
+ attachmentData = await this.fs.readBinary(attachmentPath);
4249
+ } else {
4250
+ attachmentPath = nodeFS2 && nodePath ? nodePath.join(staticPath, attachmentMeta.path) : `${staticPath}/${attachmentMeta.path}`;
4251
+ if (nodeFS2 && this.isLocalPath(staticPath)) {
4252
+ attachmentData = await nodeFS2.promises.readFile(attachmentPath);
4253
+ } else {
4254
+ const response = await fetch(attachmentPath);
4255
+ if (!response.ok) {
4256
+ result.error = `Failed to fetch attachment: ${response.status} ${response.statusText}`;
4257
+ return result;
4258
+ }
4259
+ attachmentData = await response.arrayBuffer();
4260
+ }
4261
+ }
4262
+ const doc = await db.get(docId);
4263
+ await db.putAttachment(
4264
+ docId,
4265
+ attachmentName,
4266
+ doc._rev,
4267
+ attachmentData,
4268
+ // PouchDB accepts both ArrayBuffer and Buffer
4269
+ attachmentMeta.content_type
4270
+ );
4271
+ result.success = true;
4272
+ } catch (error) {
4273
+ result.error = error instanceof Error ? error.message : String(error);
4274
+ }
4275
+ return result;
4276
+ }
4277
+ /**
4278
+ * Restore CourseConfig document from manifest
4279
+ */
4280
+ async restoreCourseConfig(manifest, targetDB) {
4281
+ const results = {
4282
+ restored: 0,
4283
+ errors: [],
4284
+ warnings: []
4285
+ };
4286
+ try {
4287
+ if (!manifest.courseConfig) {
4288
+ results.warnings.push(
4289
+ "No courseConfig found in manifest, skipping CourseConfig document creation"
4290
+ );
4291
+ return results;
4292
+ }
4293
+ const courseConfigDoc = {
4294
+ _id: "CourseConfig",
4295
+ ...manifest.courseConfig,
4296
+ courseID: manifest.courseId
4297
+ };
4298
+ delete courseConfigDoc._rev;
4299
+ await targetDB.put(courseConfigDoc);
4300
+ results.restored = 1;
4301
+ logger.info(`CourseConfig document created for course: ${manifest.courseId}`);
4302
+ } catch (error) {
4303
+ const errorMessage = error instanceof Error ? error.message : JSON.stringify(error);
4304
+ results.errors.push(`Failed to restore CourseConfig: ${errorMessage}`);
4305
+ logger.error("CourseConfig restoration failed:", error);
4306
+ }
4307
+ return results;
4308
+ }
4309
+ /**
4310
+ * Calculate expected document counts from manifest
4311
+ */
4312
+ calculateExpectedCounts(manifest) {
4313
+ const counts = {};
4314
+ for (const chunk of manifest.chunks) {
4315
+ counts[chunk.docType] = (counts[chunk.docType] || 0) + chunk.documentCount;
4316
+ }
4317
+ if (manifest.designDocs.length > 0) {
4318
+ counts["_design"] = manifest.designDocs.length;
4319
+ }
4320
+ return counts;
4321
+ }
4322
+ /**
4323
+ * Clean up database after failed migration
4324
+ */
4325
+ async cleanupFailedMigration(db) {
4326
+ logger.info("Cleaning up failed migration...");
4327
+ try {
4328
+ const allDocs = await db.allDocs();
4329
+ const docsToDelete = allDocs.rows.map((row) => ({
4330
+ _id: row.id,
4331
+ _rev: row.value.rev,
4332
+ _deleted: true
4333
+ }));
4334
+ if (docsToDelete.length > 0) {
4335
+ await db.bulkDocs(docsToDelete);
4336
+ logger.info(`Cleaned up ${docsToDelete.length} documents from failed migration`);
4337
+ }
4338
+ } catch (error) {
4339
+ logger.error("Failed to cleanup documents:", error);
4340
+ throw error;
4341
+ }
4342
+ }
4343
+ /**
4344
+ * Report progress to callback if available
4345
+ */
4346
+ reportProgress(phase, current, total, message) {
4347
+ if (this.progressCallback) {
4348
+ this.progressCallback({
4349
+ phase,
4350
+ current,
4351
+ total,
4352
+ message
4353
+ });
4354
+ }
4355
+ }
4356
+ /**
4357
+ * Check if a path is a local file path (vs URL)
4358
+ */
4359
+ isLocalPath(path2) {
4360
+ return !path2.startsWith("http://") && !path2.startsWith("https://");
4361
+ }
4362
+ };
4363
+ }
4364
+ });
4365
+
4366
+ // src/util/migrator/index.ts
4367
+ var init_migrator = __esm({
4368
+ "src/util/migrator/index.ts"() {
4369
+ "use strict";
4370
+ init_StaticToCouchDBMigrator();
4371
+ init_validation();
4372
+ init_FileSystemAdapter();
4373
+ }
4374
+ });
4375
+
4376
+ // src/util/index.ts
4377
+ var init_util2 = __esm({
4378
+ "src/util/index.ts"() {
4379
+ "use strict";
4380
+ init_Loggable();
4381
+ init_packer();
4382
+ init_migrator();
4383
+ init_dataDirectory();
4384
+ }
4385
+ });
4386
+
4387
+ // src/study/SourceMixer.ts
4388
+ var init_SourceMixer = __esm({
4389
+ "src/study/SourceMixer.ts"() {
4390
+ "use strict";
4391
+ }
4392
+ });
4393
+
4394
+ // src/study/MixerDebugger.ts
4395
+ function printMixerSummary(run) {
4396
+ console.group(`\u{1F3A8} Mixer Run: ${run.mixerType}`);
4397
+ logger.info(`Run ID: ${run.runId}`);
4398
+ logger.info(`Time: ${run.timestamp.toISOString()}`);
4399
+ logger.info(
4400
+ `Config: limit=${run.requestedLimit}${run.quotaPerSource ? `, quota/source=${run.quotaPerSource}` : ""}`
4401
+ );
4402
+ console.group(`\u{1F4E5} Input: ${run.sourceSummaries.length} sources`);
4403
+ for (const src of run.sourceSummaries) {
4404
+ logger.info(
4405
+ ` ${src.sourceName || src.sourceId}: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`
4406
+ );
4407
+ logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}], avg: ${src.avgScore.toFixed(2)}`);
4408
+ }
4409
+ console.groupEnd();
4410
+ console.group(`\u{1F4E4} Output: ${run.finalCount} cards selected (${run.reviewsSelected} reviews, ${run.newSelected} new)`);
4411
+ for (const breakdown of run.sourceBreakdowns) {
4412
+ const name = breakdown.sourceName || breakdown.sourceId;
4413
+ logger.info(
4414
+ ` ${name}: ${breakdown.totalSelected} selected (${breakdown.reviewsSelected} reviews, ${breakdown.newSelected} new) - ${breakdown.selectionRate.toFixed(1)}% selection rate`
4415
+ );
4416
+ }
4417
+ console.groupEnd();
4418
+ console.groupEnd();
4419
+ }
4420
+ function mountMixerDebugger() {
4421
+ if (typeof window === "undefined") return;
4422
+ const win = window;
4423
+ win.skuilder = win.skuilder || {};
4424
+ win.skuilder.mixer = mixerDebugAPI;
4425
+ }
4426
+ var runHistory2, mixerDebugAPI;
4427
+ var init_MixerDebugger = __esm({
4428
+ "src/study/MixerDebugger.ts"() {
4429
+ "use strict";
4430
+ init_logger();
4431
+ init_navigators();
4432
+ runHistory2 = [];
4433
+ mixerDebugAPI = {
4434
+ /**
4435
+ * Get raw run history for programmatic access.
4436
+ */
4437
+ get runs() {
4438
+ return [...runHistory2];
4439
+ },
4440
+ /**
4441
+ * Show summary of a specific mixer run.
4442
+ */
4443
+ showRun(idOrIndex = 0) {
4444
+ if (runHistory2.length === 0) {
4445
+ logger.info("[Mixer Debug] No runs captured yet.");
4446
+ return;
4447
+ }
4448
+ let run;
4449
+ if (typeof idOrIndex === "number") {
4450
+ run = runHistory2[idOrIndex];
4451
+ if (!run) {
4452
+ logger.info(`[Mixer Debug] No run found at index ${idOrIndex}. History length: ${runHistory2.length}`);
4453
+ return;
4454
+ }
4455
+ } else {
4456
+ run = runHistory2.find((r) => r.runId.endsWith(idOrIndex));
4457
+ if (!run) {
4458
+ logger.info(`[Mixer Debug] No run found matching ID '${idOrIndex}'.`);
4459
+ return;
4460
+ }
4461
+ }
4462
+ printMixerSummary(run);
4463
+ },
4464
+ /**
4465
+ * Show summary of the last mixer run.
4466
+ */
4467
+ showLastMix() {
4468
+ this.showRun(0);
4469
+ },
4470
+ /**
4471
+ * Explain source balance in the last run.
4472
+ */
4473
+ explainSourceBalance() {
4474
+ if (runHistory2.length === 0) {
4475
+ logger.info("[Mixer Debug] No runs captured yet.");
4476
+ return;
4477
+ }
4478
+ const run = runHistory2[0];
4479
+ console.group("\u2696\uFE0F Source Balance Analysis");
4480
+ logger.info(`Mixer: ${run.mixerType}`);
4481
+ logger.info(`Requested limit: ${run.requestedLimit}`);
4482
+ if (run.quotaPerSource) {
4483
+ logger.info(`Quota per source: ${run.quotaPerSource}`);
4484
+ }
4485
+ console.group("Input Distribution:");
4486
+ for (const src of run.sourceSummaries) {
4487
+ const name = src.sourceName || src.sourceId;
4488
+ logger.info(`${name}:`);
4489
+ logger.info(` Provided: ${src.totalCards} cards (${src.reviewCount} reviews, ${src.newCount} new)`);
4490
+ logger.info(` Score range: [${src.scoreRange[0].toFixed(2)}, ${src.scoreRange[1].toFixed(2)}]`);
4491
+ }
4492
+ console.groupEnd();
4493
+ console.group("Selection Results:");
4494
+ for (const breakdown of run.sourceBreakdowns) {
4495
+ const name = breakdown.sourceName || breakdown.sourceId;
4496
+ logger.info(`${name}:`);
4497
+ logger.info(
4498
+ ` Selected: ${breakdown.totalSelected}/${breakdown.reviewsProvided + breakdown.newProvided} (${breakdown.selectionRate.toFixed(1)}%)`
4499
+ );
4500
+ logger.info(` Reviews: ${breakdown.reviewsSelected}/${breakdown.reviewsProvided}`);
4501
+ logger.info(` New: ${breakdown.newSelected}/${breakdown.newProvided}`);
4502
+ if (breakdown.reviewsProvided > 0 && breakdown.reviewsSelected === 0) {
4503
+ logger.info(` \u26A0\uFE0F Had reviews but none selected!`);
4504
+ }
4505
+ if (breakdown.totalSelected === 0 && breakdown.reviewsProvided + breakdown.newProvided > 0) {
4506
+ logger.info(` \u26A0\uFE0F Had cards but none selected!`);
4507
+ }
4508
+ }
4509
+ console.groupEnd();
4510
+ const selectionRates = run.sourceBreakdowns.map((b) => b.selectionRate);
4511
+ const avgRate = selectionRates.reduce((a, b) => a + b, 0) / selectionRates.length;
4512
+ const maxDeviation = Math.max(...selectionRates.map((r) => Math.abs(r - avgRate)));
4513
+ if (maxDeviation > 20) {
4514
+ logger.info(`
4515
+ \u26A0\uFE0F Significant imbalance detected (max deviation: ${maxDeviation.toFixed(1)}%)`);
4516
+ logger.info("Possible causes:");
4517
+ logger.info(" - Score range differences between sources");
4518
+ logger.info(" - One source has much better quality cards");
4519
+ logger.info(" - Different card availability (reviews vs new)");
4520
+ }
4521
+ console.groupEnd();
4522
+ },
4523
+ /**
4524
+ * Compare score distributions across sources.
4525
+ */
4526
+ compareScores() {
4527
+ if (runHistory2.length === 0) {
4528
+ logger.info("[Mixer Debug] No runs captured yet.");
4529
+ return;
4530
+ }
4531
+ const run = runHistory2[0];
4532
+ console.group("\u{1F4CA} Score Distribution Comparison");
4533
+ console.table(
4534
+ run.sourceSummaries.map((src) => ({
4535
+ source: src.sourceName || src.sourceId,
4536
+ cards: src.totalCards,
4537
+ min: src.bottomScore.toFixed(3),
4538
+ max: src.topScore.toFixed(3),
4539
+ avg: src.avgScore.toFixed(3),
4540
+ range: (src.topScore - src.bottomScore).toFixed(3)
4541
+ }))
4542
+ );
4543
+ const ranges = run.sourceSummaries.map((s) => s.topScore - s.bottomScore);
4544
+ const avgScores = run.sourceSummaries.map((s) => s.avgScore);
4545
+ const rangeDiff = Math.max(...ranges) - Math.min(...ranges);
4546
+ const avgDiff = Math.max(...avgScores) - Math.min(...avgScores);
4547
+ if (rangeDiff > 0.3 || avgDiff > 0.2) {
4548
+ logger.info("\n\u26A0\uFE0F Significant score distribution differences detected");
4549
+ logger.info(
4550
+ "This may cause one source to dominate selection if using global sorting (not quota-based)"
4551
+ );
4552
+ }
4553
+ console.groupEnd();
4554
+ },
4555
+ /**
4556
+ * Show detailed information for a specific card.
4557
+ */
4558
+ showCard(cardId) {
4559
+ for (const run of runHistory2) {
4560
+ const card = run.cards.find((c) => c.cardId === cardId);
4561
+ if (card) {
4562
+ const source = run.sourceSummaries.find((s) => s.sourceIndex === card.sourceIndex);
4563
+ console.group(`\u{1F3B4} Card: ${cardId}`);
4564
+ logger.info(`Course: ${card.courseId}`);
4565
+ logger.info(`Source: ${source?.sourceName || source?.sourceId || "unknown"}`);
4566
+ logger.info(`Origin: ${card.origin}`);
4567
+ logger.info(`Score: ${card.score.toFixed(3)}`);
4568
+ if (card.rankInSource) {
4569
+ logger.info(`Rank in source: #${card.rankInSource}`);
4570
+ }
4571
+ if (card.rankInMix) {
4572
+ logger.info(`Rank in mixed results: #${card.rankInMix}`);
4573
+ }
4574
+ logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
4575
+ if (!card.selected && card.rankInSource) {
4576
+ logger.info("\nWhy not selected:");
4577
+ if (run.quotaPerSource && card.rankInSource > run.quotaPerSource) {
4578
+ logger.info(` - Ranked #${card.rankInSource} in source, but quota was ${run.quotaPerSource}`);
4579
+ }
4580
+ logger.info(" - Check score compared to selected cards using .showRun()");
4581
+ }
4582
+ console.groupEnd();
4583
+ return;
4584
+ }
4585
+ }
4586
+ logger.info(`[Mixer Debug] Card '${cardId}' not found in recent runs.`);
4587
+ },
4588
+ /**
4589
+ * Show all runs in compact format.
4590
+ */
4591
+ listRuns() {
4592
+ if (runHistory2.length === 0) {
4593
+ logger.info("[Mixer Debug] No runs captured yet.");
4594
+ return;
4595
+ }
4596
+ console.table(
4597
+ runHistory2.map((r) => ({
4598
+ id: r.runId.slice(-8),
4599
+ time: r.timestamp.toLocaleTimeString(),
4600
+ mixer: r.mixerType,
4601
+ sources: r.sourceSummaries.length,
4602
+ selected: r.finalCount,
4603
+ reviews: r.reviewsSelected,
4604
+ new: r.newSelected
4605
+ }))
4606
+ );
4607
+ },
4608
+ /**
4609
+ * Export run history as JSON for bug reports.
4610
+ */
4611
+ export() {
4612
+ const json = JSON.stringify(runHistory2, null, 2);
4613
+ logger.info("[Mixer Debug] Run history exported. Copy the returned string or use:");
4614
+ logger.info(" copy(window.skuilder.mixer.export())");
4615
+ return json;
4616
+ },
4617
+ /**
4618
+ * Clear run history.
4619
+ */
4620
+ clear() {
4621
+ runHistory2.length = 0;
4622
+ logger.info("[Mixer Debug] Run history cleared.");
4623
+ },
4624
+ /**
4625
+ * Show help.
4626
+ */
4627
+ help() {
4628
+ logger.info(`
4629
+ \u{1F3A8} Mixer Debug API
4630
+
4631
+ Commands:
4632
+ .showLastMix() Show summary of most recent mixer run
4633
+ .showRun(id|index) Show summary of a specific run (by index or ID suffix)
4634
+ .explainSourceBalance() Analyze source balance and selection patterns
4635
+ .compareScores() Compare score distributions across sources
4636
+ .showCard(cardId) Show mixer decisions for a specific card
4637
+ .listRuns() List all captured runs in table format
4638
+ .export() Export run history as JSON for bug reports
4639
+ .clear() Clear run history
4640
+ .runs Access raw run history array
4641
+ .help() Show this help message
4642
+
4643
+ Example:
4644
+ window.skuilder.mixer.showLastMix()
4645
+ window.skuilder.mixer.explainSourceBalance()
4646
+ window.skuilder.mixer.compareScores()
4647
+ `);
4648
+ }
4649
+ };
4650
+ mountMixerDebugger();
4651
+ }
4652
+ });
4653
+
4654
+ // src/study/SessionDebugger.ts
4655
+ function showCurrentQueue() {
4656
+ if (!activeSession) {
4657
+ logger.info("[Session Debug] No active session.");
4658
+ return;
2723
4659
  }
2724
- const target = config.targetAccuracy ?? 0.85;
2725
- const tolerance = config.tolerance ?? 0.05;
2726
- let correct = 0;
2727
- for (const r of records) {
2728
- if (r.isCorrect) correct++;
4660
+ const latest = activeSession.queueSnapshots[activeSession.queueSnapshots.length - 1] || activeSession.initialQueues;
4661
+ console.group("\u{1F4CA} Current Queue State");
4662
+ logger.info(`Review Queue: ${latest.reviewQLength} cards`);
4663
+ if (latest.reviewQNext3 && latest.reviewQNext3.length > 0) {
4664
+ logger.info(` Next: ${latest.reviewQNext3.join(", ")}`);
2729
4665
  }
2730
- const accuracy = correct / records.length;
2731
- return scoreAccuracyInZone(accuracy, target, tolerance);
2732
- }
2733
- function scoreAccuracyInZone(accuracy, target, tolerance) {
2734
- const dist = Math.abs(accuracy - target);
2735
- if (dist <= tolerance) {
2736
- return 1;
4666
+ logger.info(`New Queue: ${latest.newQLength} cards`);
4667
+ if (latest.newQNext3 && latest.newQNext3.length > 0) {
4668
+ logger.info(` Next: ${latest.newQNext3.join(", ")}`);
2737
4669
  }
2738
- const excess = dist - tolerance;
2739
- const slope = 2.5;
2740
- return Math.max(0, 1 - excess * slope);
4670
+ logger.info(`Failed Queue: ${latest.failedQLength} cards`);
4671
+ console.groupEnd();
2741
4672
  }
2742
- var init_signal = __esm({
2743
- "src/core/orchestration/signal.ts"() {
2744
- "use strict";
4673
+ function showPresentationHistory(sessionIndex = 0) {
4674
+ const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
4675
+ if (!session) {
4676
+ logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
4677
+ return;
2745
4678
  }
2746
- });
2747
-
2748
- // src/core/orchestration/recording.ts
2749
- async function recordUserOutcome(context, periodStart, periodEnd, records, activeStrategyIds, eloStart = 0, eloEnd = 0, config) {
2750
- const { user, course, userId } = context;
2751
- const courseId = course.getCourseID();
2752
- const outcomeValue = computeOutcomeSignal(records, config);
2753
- if (outcomeValue === null) {
2754
- logger.debug(
2755
- `[Orchestration] No outcome signal computed for ${userId} (insufficient data). Skipping record.`
4679
+ console.group(`\u{1F4DC} Session History: ${session.sessionId}`);
4680
+ logger.info(`Started: ${session.startTime.toLocaleTimeString()}`);
4681
+ if (session.endTime) {
4682
+ logger.info(`Ended: ${session.endTime.toLocaleTimeString()}`);
4683
+ }
4684
+ logger.info(`Cards presented: ${session.presentations.length}`);
4685
+ if (session.presentations.length > 0) {
4686
+ console.table(
4687
+ session.presentations.map((p) => ({
4688
+ "#": p.sequenceNumber,
4689
+ course: p.courseName || p.courseId.slice(0, 8),
4690
+ origin: p.origin,
4691
+ queue: p.queueSource,
4692
+ score: p.score?.toFixed(3) || "-",
4693
+ time: p.timestamp.toLocaleTimeString()
4694
+ }))
2756
4695
  );
4696
+ }
4697
+ console.groupEnd();
4698
+ }
4699
+ function showInterleaving(sessionIndex = 0) {
4700
+ const session = sessionIndex === 0 && activeSession ? activeSession : sessionHistory[sessionIndex];
4701
+ if (!session) {
4702
+ logger.info(`[Session Debug] No session found at index ${sessionIndex}`);
2757
4703
  return;
2758
4704
  }
2759
- const deviations = {};
2760
- for (const strategyId of activeStrategyIds) {
2761
- deviations[strategyId] = context.getDeviation(strategyId);
4705
+ console.group("\u{1F500} Interleaving Analysis");
4706
+ const courseCounts = /* @__PURE__ */ new Map();
4707
+ const courseOrigins = /* @__PURE__ */ new Map();
4708
+ session.presentations.forEach((p) => {
4709
+ const name = p.courseName || p.courseId;
4710
+ courseCounts.set(name, (courseCounts.get(name) || 0) + 1);
4711
+ if (!courseOrigins.has(name)) {
4712
+ courseOrigins.set(name, { review: 0, new: 0, failed: 0 });
4713
+ }
4714
+ const origins = courseOrigins.get(name);
4715
+ origins[p.origin]++;
4716
+ });
4717
+ logger.info("Course distribution:");
4718
+ console.table(
4719
+ Array.from(courseCounts.entries()).map(([course, count]) => {
4720
+ const origins = courseOrigins.get(course);
4721
+ return {
4722
+ course,
4723
+ total: count,
4724
+ reviews: origins.review,
4725
+ new: origins.new,
4726
+ failed: origins.failed,
4727
+ percentage: (count / session.presentations.length * 100).toFixed(1) + "%"
4728
+ };
4729
+ })
4730
+ );
4731
+ if (session.presentations.length > 0) {
4732
+ logger.info("\nPresentation sequence (first 20):");
4733
+ const sequence = session.presentations.slice(0, 20).map((p, idx) => `${idx + 1}. ${p.courseName || p.courseId.slice(0, 8)} (${p.origin})`).join("\n");
4734
+ logger.info(sequence);
2762
4735
  }
2763
- const id = `USER_OUTCOME::${courseId}::${userId}::${periodEnd}`;
2764
- const record = {
2765
- _id: id,
2766
- docType: "USER_OUTCOME" /* USER_OUTCOME */,
2767
- courseId,
2768
- userId,
2769
- periodStart,
2770
- periodEnd,
2771
- outcomeValue,
2772
- deviations,
2773
- metadata: {
2774
- sessionsCount: 1,
2775
- // Assumes recording is triggered per-session currently
2776
- cardsSeen: records.length,
2777
- eloStart,
2778
- eloEnd,
2779
- signalType: "accuracy_in_zone"
4736
+ let maxCluster = 0;
4737
+ let currentCluster = 1;
4738
+ let currentCourse = session.presentations[0]?.courseId;
4739
+ for (let i = 1; i < session.presentations.length; i++) {
4740
+ if (session.presentations[i].courseId === currentCourse) {
4741
+ currentCluster++;
4742
+ maxCluster = Math.max(maxCluster, currentCluster);
4743
+ } else {
4744
+ currentCourse = session.presentations[i].courseId;
4745
+ currentCluster = 1;
2780
4746
  }
2781
- };
2782
- try {
2783
- await user.putUserOutcome(record);
2784
- logger.debug(
2785
- `[Orchestration] Recorded outcome ${outcomeValue.toFixed(3)} for ${userId} (doc: ${id})`
2786
- );
2787
- } catch (e) {
2788
- logger.error(`[Orchestration] Failed to record outcome: ${e}`);
2789
4747
  }
4748
+ if (maxCluster > 3) {
4749
+ logger.info(`
4750
+ \u26A0\uFE0F Detected clustering: max ${maxCluster} cards from same course in a row`);
4751
+ logger.info("This suggests cards are sorted by score rather than round-robin by course.");
4752
+ }
4753
+ console.groupEnd();
2790
4754
  }
2791
- var init_recording = __esm({
2792
- "src/core/orchestration/recording.ts"() {
4755
+ function mountSessionDebugger() {
4756
+ if (typeof window === "undefined") return;
4757
+ const win = window;
4758
+ win.skuilder = win.skuilder || {};
4759
+ win.skuilder.session = sessionDebugAPI;
4760
+ }
4761
+ var activeSession, sessionHistory, sessionDebugAPI;
4762
+ var init_SessionDebugger = __esm({
4763
+ "src/study/SessionDebugger.ts"() {
2793
4764
  "use strict";
2794
- init_signal();
2795
- init_types_legacy();
2796
4765
  init_logger();
2797
- }
2798
- });
4766
+ activeSession = null;
4767
+ sessionHistory = [];
4768
+ sessionDebugAPI = {
4769
+ /**
4770
+ * Get raw session history for programmatic access.
4771
+ */
4772
+ get sessions() {
4773
+ return [...sessionHistory];
4774
+ },
4775
+ /**
4776
+ * Get active session if any.
4777
+ */
4778
+ get active() {
4779
+ return activeSession;
4780
+ },
4781
+ /**
4782
+ * Show current queue state.
4783
+ */
4784
+ showQueue() {
4785
+ showCurrentQueue();
4786
+ },
4787
+ /**
4788
+ * Show presentation history for current or past session.
4789
+ */
4790
+ showHistory(sessionIndex = 0) {
4791
+ showPresentationHistory(sessionIndex);
4792
+ },
4793
+ /**
4794
+ * Analyze course interleaving pattern.
4795
+ */
4796
+ showInterleaving(sessionIndex = 0) {
4797
+ showInterleaving(sessionIndex);
4798
+ },
4799
+ /**
4800
+ * List all tracked sessions.
4801
+ */
4802
+ listSessions() {
4803
+ if (activeSession) {
4804
+ logger.info(`Active session: ${activeSession.sessionId} (${activeSession.presentations.length} cards presented)`);
4805
+ }
4806
+ if (sessionHistory.length === 0) {
4807
+ logger.info("[Session Debug] No completed sessions in history.");
4808
+ return;
4809
+ }
4810
+ console.table(
4811
+ sessionHistory.map((s, idx) => ({
4812
+ index: idx,
4813
+ id: s.sessionId.slice(-8),
4814
+ started: s.startTime.toLocaleTimeString(),
4815
+ ended: s.endTime?.toLocaleTimeString() || "incomplete",
4816
+ cards: s.presentations.length
4817
+ }))
4818
+ );
4819
+ },
4820
+ /**
4821
+ * Export session history as JSON for bug reports.
4822
+ */
4823
+ export() {
4824
+ const data = {
4825
+ active: activeSession,
4826
+ history: sessionHistory
4827
+ };
4828
+ const json = JSON.stringify(data, null, 2);
4829
+ logger.info("[Session Debug] Session data exported. Copy the returned string or use:");
4830
+ logger.info(" copy(window.skuilder.session.export())");
4831
+ return json;
4832
+ },
4833
+ /**
4834
+ * Clear session history.
4835
+ */
4836
+ clear() {
4837
+ sessionHistory.length = 0;
4838
+ logger.info("[Session Debug] Session history cleared.");
4839
+ },
4840
+ /**
4841
+ * Show help.
4842
+ */
4843
+ help() {
4844
+ logger.info(`
4845
+ \u{1F3AF} Session Debug API
2799
4846
 
2800
- // src/core/orchestration/index.ts
2801
- function fnv1a(str) {
2802
- let hash = 2166136261;
2803
- for (let i = 0; i < str.length; i++) {
2804
- hash ^= str.charCodeAt(i);
2805
- hash = Math.imul(hash, 16777619);
2806
- }
2807
- return hash >>> 0;
2808
- }
2809
- function computeDeviation(userId, strategyId, salt) {
2810
- const input = `${userId}:${strategyId}:${salt}`;
2811
- const hash = fnv1a(input);
2812
- const normalized = hash / 4294967296;
2813
- return normalized * 2 - 1;
2814
- }
2815
- function computeSpread(confidence) {
2816
- const clampedConfidence = Math.max(0, Math.min(1, confidence));
2817
- return MAX_SPREAD - clampedConfidence * (MAX_SPREAD - MIN_SPREAD);
2818
- }
2819
- function computeEffectiveWeight(learnable, userId, strategyId, salt) {
2820
- const deviation = computeDeviation(userId, strategyId, salt);
2821
- const spread = computeSpread(learnable.confidence);
2822
- const adjustment = deviation * spread * learnable.weight;
2823
- const effective = learnable.weight + adjustment;
2824
- return Math.max(MIN_WEIGHT, Math.min(MAX_WEIGHT, effective));
2825
- }
2826
- async function createOrchestrationContext(user, course) {
2827
- let courseConfig;
2828
- try {
2829
- courseConfig = await course.getCourseConfig();
2830
- } catch (e) {
2831
- logger.error(`[Orchestration] Failed to load course config: ${e}`);
2832
- courseConfig = {
2833
- name: "Unknown",
2834
- description: "",
2835
- public: false,
2836
- deleted: false,
2837
- creator: "",
2838
- admins: [],
2839
- moderators: [],
2840
- dataShapes: [],
2841
- questionTypes: [],
2842
- orchestration: { salt: "default" }
4847
+ Commands:
4848
+ .showQueue() Show current queue state (active session only)
4849
+ .showHistory(index?) Show presentation history (0=current/last, 1=previous, etc)
4850
+ .showInterleaving(index?) Analyze course interleaving pattern
4851
+ .listSessions() List all tracked sessions
4852
+ .export() Export session data as JSON for bug reports
4853
+ .clear() Clear session history
4854
+ .sessions Access raw session history array
4855
+ .active Access active session (if any)
4856
+ .help() Show this help message
4857
+
4858
+ Example:
4859
+ window.skuilder.session.showHistory()
4860
+ window.skuilder.session.showInterleaving()
4861
+ window.skuilder.session.showQueue()
4862
+ `);
4863
+ }
2843
4864
  };
4865
+ mountSessionDebugger();
2844
4866
  }
2845
- const userId = user.getUsername();
2846
- const salt = courseConfig.orchestration?.salt || "default_salt";
2847
- return {
2848
- user,
2849
- course,
2850
- userId,
2851
- courseConfig,
2852
- getEffectiveWeight(strategyId, learnable) {
2853
- return computeEffectiveWeight(learnable, userId, strategyId, salt);
2854
- },
2855
- getDeviation(strategyId) {
2856
- return computeDeviation(userId, strategyId, salt);
2857
- }
2858
- };
2859
- }
2860
- var MIN_SPREAD, MAX_SPREAD, MIN_WEIGHT, MAX_WEIGHT;
2861
- var init_orchestration = __esm({
2862
- "src/core/orchestration/index.ts"() {
4867
+ });
4868
+
4869
+ // src/study/SessionController.ts
4870
+ var init_SessionController = __esm({
4871
+ "src/study/SessionController.ts"() {
2863
4872
  "use strict";
2864
- init_logger();
2865
- init_gradient();
2866
- init_learning();
2867
- init_signal();
4873
+ init_SrsService();
4874
+ init_EloService();
4875
+ init_ResponseProcessor();
4876
+ init_CardHydrationService();
4877
+ init_ItemQueue();
4878
+ init_couch();
2868
4879
  init_recording();
2869
- MIN_SPREAD = 0.1;
2870
- MAX_SPREAD = 0.5;
2871
- MIN_WEIGHT = 0.1;
2872
- MAX_WEIGHT = 3;
4880
+ init_util2();
4881
+ init_navigators();
4882
+ init_SourceMixer();
4883
+ init_MixerDebugger();
4884
+ init_SessionDebugger();
4885
+ init_logger();
2873
4886
  }
2874
4887
  });
2875
4888
 
@@ -2878,7 +4891,7 @@ var Pipeline_exports = {};
2878
4891
  __export(Pipeline_exports, {
2879
4892
  Pipeline: () => Pipeline
2880
4893
  });
2881
- import { toCourseElo as toCourseElo5 } from "@vue-skuilder/common";
4894
+ import { toCourseElo as toCourseElo7 } from "@vue-skuilder/common";
2882
4895
  function globToRegex(pattern) {
2883
4896
  const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
2884
4897
  const withWildcards = escaped.replace(/\*/g, ".*");
@@ -2962,6 +4975,7 @@ var init_Pipeline = __esm({
2962
4975
  init_logger();
2963
4976
  init_orchestration();
2964
4977
  init_PipelineDebugger();
4978
+ init_SessionController();
2965
4979
  VERBOSE_RESULTS = true;
2966
4980
  Pipeline = class extends ContentNavigator {
2967
4981
  generator;
@@ -3121,8 +5135,9 @@ var init_Pipeline = __esm({
3121
5135
  generatorSummaries,
3122
5136
  generatedCount,
3123
5137
  filterImpacts,
3124
- allCardsBeforeFiltering,
3125
- result
5138
+ cards,
5139
+ result,
5140
+ context.userElo
3126
5141
  );
3127
5142
  captureRun(report);
3128
5143
  } catch (e) {
@@ -3283,7 +5298,7 @@ var init_Pipeline = __esm({
3283
5298
  let userElo = 1e3;
3284
5299
  try {
3285
5300
  const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
3286
- const courseElo = toCourseElo5(courseReg.elo);
5301
+ const courseElo = toCourseElo7(courseReg.elo);
3287
5302
  userElo = courseElo.global.score;
3288
5303
  } catch (e) {
3289
5304
  logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
@@ -3336,6 +5351,34 @@ var init_Pipeline = __esm({
3336
5351
  return [...new Set(ids)];
3337
5352
  }
3338
5353
  // ---------------------------------------------------------------------------
5354
+ // Tag ELO diagnostic
5355
+ // ---------------------------------------------------------------------------
5356
+ /**
5357
+ * Get the user's per-tag ELO data for specified tags (or all tags).
5358
+ * Useful for diagnosing why hierarchy gates are open/closed.
5359
+ */
5360
+ async getTagEloStatus(tagFilter) {
5361
+ const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
5362
+ const courseElo = toCourseElo7(courseReg.elo);
5363
+ const result = {};
5364
+ if (!tagFilter) {
5365
+ for (const [tag, data] of Object.entries(courseElo.tags)) {
5366
+ result[tag] = { score: data.score, count: data.count };
5367
+ }
5368
+ } else {
5369
+ const patterns = Array.isArray(tagFilter) ? tagFilter : [tagFilter];
5370
+ for (const pattern of patterns) {
5371
+ const regex = globToRegex(pattern);
5372
+ for (const [tag, data] of Object.entries(courseElo.tags)) {
5373
+ if (regex.test(tag)) {
5374
+ result[tag] = { score: data.score, count: data.count };
5375
+ }
5376
+ }
5377
+ }
5378
+ }
5379
+ return result;
5380
+ }
5381
+ // ---------------------------------------------------------------------------
3339
5382
  // Card-space diagnostic
3340
5383
  // ---------------------------------------------------------------------------
3341
5384
  /**
@@ -3918,7 +5961,7 @@ import {
3918
5961
  EloToNumber,
3919
5962
  Status,
3920
5963
  blankCourseElo as blankCourseElo2,
3921
- toCourseElo as toCourseElo6
5964
+ toCourseElo as toCourseElo8
3922
5965
  } from "@vue-skuilder/common";
3923
5966
  function randIntWeightedTowardZero(n) {
3924
5967
  return Math.floor(Math.random() * Math.random() * Math.random() * n);
@@ -4102,7 +6145,7 @@ var init_courseDB = __esm({
4102
6145
  docs.rows.forEach((r) => {
4103
6146
  if (isSuccessRow(r)) {
4104
6147
  if (r.doc && r.doc.elo) {
4105
- ret.push(toCourseElo6(r.doc.elo));
6148
+ ret.push(toCourseElo8(r.doc.elo));
4106
6149
  } else {
4107
6150
  logger.warn("no elo data for card: " + r.id);
4108
6151
  ret.push(blankCourseElo2());
@@ -4607,7 +6650,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
4607
6650
  });
4608
6651
 
4609
6652
  // src/impl/couch/classroomDB.ts
4610
- import moment4 from "moment";
6653
+ import moment6 from "moment";
4611
6654
  var CLASSROOM_CONFIG, ClassroomDBBase, StudentClassroomDB;
4612
6655
  var init_classroomDB2 = __esm({
4613
6656
  "src/impl/couch/classroomDB.ts"() {
@@ -4725,9 +6768,9 @@ var init_classroomDB2 = __esm({
4725
6768
  }
4726
6769
  const activeCards = await this._user.getActiveCards();
4727
6770
  const activeCardIds = new Set(activeCards.map((ac) => ac.cardID));
4728
- const now = moment4.utc();
6771
+ const now = moment6.utc();
4729
6772
  const assigned = await this.getAssignedContent();
4730
- const due = assigned.filter((c) => now.isAfter(moment4.utc(c.activeOn, REVIEW_TIME_FORMAT2)));
6773
+ const due = assigned.filter((c) => now.isAfter(moment6.utc(c.activeOn, REVIEW_TIME_FORMAT2)));
4731
6774
  logger.info(`[StudentClassroomDB] Due content: ${JSON.stringify(due)}`);
4732
6775
  for (const content of due) {
4733
6776
  if (content.type === "course") {
@@ -4825,7 +6868,7 @@ var init_CourseSyncService = __esm({
4825
6868
  });
4826
6869
 
4827
6870
  // src/impl/couch/auth.ts
4828
- import fetch from "cross-fetch";
6871
+ import fetch2 from "cross-fetch";
4829
6872
  var init_auth = __esm({
4830
6873
  "src/impl/couch/auth.ts"() {
4831
6874
  "use strict";
@@ -4850,8 +6893,8 @@ var init_CouchDBSyncStrategy = __esm({
4850
6893
  });
4851
6894
 
4852
6895
  // src/impl/couch/index.ts
4853
- import fetch2 from "cross-fetch";
4854
- import moment5 from "moment";
6896
+ import fetch3 from "cross-fetch";
6897
+ import moment7 from "moment";
4855
6898
  import process2 from "process";
4856
6899
  function createPouchDBConfig() {
4857
6900
  const hasExplicitCredentials = ENV.COUCHDB_USERNAME && ENV.COUCHDB_PASSWORD;
@@ -4928,7 +6971,7 @@ var init_couch = __esm({
4928
6971
 
4929
6972
  // src/impl/common/BaseUserDB.ts
4930
6973
  import { Status as Status3 } from "@vue-skuilder/common";
4931
- import moment6 from "moment";
6974
+ import moment8 from "moment";
4932
6975
  async function getOrCreateClassroomRegistrationsDoc(user) {
4933
6976
  let ret;
4934
6977
  try {
@@ -5290,7 +7333,7 @@ Currently logged-in as ${this._username}.`
5290
7333
  );
5291
7334
  return reviews.rows.filter((r) => {
5292
7335
  if (r.id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */])) {
5293
- const date = moment6.utc(
7336
+ const date = moment8.utc(
5294
7337
  r.id.substr(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */].length),
5295
7338
  REVIEW_TIME_FORMAT
5296
7339
  );
@@ -5303,11 +7346,11 @@ Currently logged-in as ${this._username}.`
5303
7346
  }).map((r) => r.doc);
5304
7347
  }
5305
7348
  async getReviewsForcast(daysCount) {
5306
- const time = moment6.utc().add(daysCount, "days");
7349
+ const time = moment8.utc().add(daysCount, "days");
5307
7350
  return this.getReviewstoDate(time);
5308
7351
  }
5309
7352
  async getPendingReviews(course_id) {
5310
- const now = moment6.utc();
7353
+ const now = moment8.utc();
5311
7354
  return this.getReviewstoDate(now, course_id);
5312
7355
  }
5313
7356
  async getScheduledReviewCount(course_id) {
@@ -5594,7 +7637,7 @@ Currently logged-in as ${this._username}.`
5594
7637
  */
5595
7638
  async putCardRecord(record) {
5596
7639
  const cardHistoryID = getCardHistoryID(record.courseID, record.cardID);
5597
- record.timeStamp = moment6.utc(record.timeStamp).toString();
7640
+ record.timeStamp = moment8.utc(record.timeStamp).toString();
5598
7641
  try {
5599
7642
  const cardHistory = await this.update(
5600
7643
  cardHistoryID,
@@ -5610,7 +7653,7 @@ Currently logged-in as ${this._username}.`
5610
7653
  const ret = {
5611
7654
  ...record2
5612
7655
  };
5613
- ret.timeStamp = moment6.utc(record2.timeStamp);
7656
+ ret.timeStamp = moment8.utc(record2.timeStamp);
5614
7657
  return ret;
5615
7658
  });
5616
7659
  return cardHistory;
@@ -6310,7 +8353,7 @@ var init_cardProcessor = __esm({
6310
8353
  });
6311
8354
 
6312
8355
  // src/core/bulkImport/types.ts
6313
- var init_types3 = __esm({
8356
+ var init_types5 = __esm({
6314
8357
  "src/core/bulkImport/types.ts"() {
6315
8358
  "use strict";
6316
8359
  }
@@ -6321,7 +8364,7 @@ var init_bulkImport = __esm({
6321
8364
  "src/core/bulkImport/index.ts"() {
6322
8365
  "use strict";
6323
8366
  init_cardProcessor();
6324
- init_types3();
8367
+ init_types5();
6325
8368
  }
6326
8369
  });
6327
8370