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