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