@vue-skuilder/db 0.1.32-b → 0.1.32-e

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 (51) hide show
  1. package/dist/{contentSource-DF1nUbPQ.d.cts → contentSource-BMlMwSiG.d.cts} +124 -5
  2. package/dist/{contentSource-Bdwkvqa8.d.ts → contentSource-Ht3N2f-y.d.ts} +124 -5
  3. package/dist/core/index.d.cts +26 -83
  4. package/dist/core/index.d.ts +26 -83
  5. package/dist/core/index.js +767 -71
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +767 -71
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BQdfJuBN.d.cts → dataLayerProvider-BEqB8VBR.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-BKmVoyJR.d.ts → dataLayerProvider-DObSXjnf.d.ts} +1 -1
  11. package/dist/impl/couch/index.d.cts +18 -5
  12. package/dist/impl/couch/index.d.ts +18 -5
  13. package/dist/impl/couch/index.js +817 -74
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +817 -74
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.cts +4 -4
  18. package/dist/impl/static/index.d.ts +4 -4
  19. package/dist/impl/static/index.js +763 -67
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +763 -67
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/index.d.cts +23 -8
  24. package/dist/index.d.ts +23 -8
  25. package/dist/index.js +872 -86
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +872 -86
  28. package/dist/index.mjs.map +1 -1
  29. package/docs/navigators-architecture.md +2 -2
  30. package/package.json +2 -2
  31. package/src/core/interfaces/contentSource.ts +3 -3
  32. package/src/core/navigators/Pipeline.ts +104 -32
  33. package/src/core/navigators/PipelineDebugger.ts +152 -1
  34. package/src/core/navigators/filters/hierarchyDefinition.ts +90 -6
  35. package/src/core/navigators/filters/interferenceMitigator.ts +2 -1
  36. package/src/core/navigators/filters/relativePriority.ts +2 -1
  37. package/src/core/navigators/filters/userTagPreference.ts +2 -1
  38. package/src/core/navigators/generators/CompositeGenerator.ts +58 -5
  39. package/src/core/navigators/generators/elo.ts +7 -7
  40. package/src/core/navigators/generators/prescribed.ts +710 -46
  41. package/src/core/navigators/generators/srs.ts +3 -4
  42. package/src/core/navigators/generators/types.ts +48 -2
  43. package/src/core/navigators/index.ts +4 -3
  44. package/src/impl/couch/CourseSyncService.ts +72 -4
  45. package/src/impl/couch/classroomDB.ts +4 -3
  46. package/src/impl/couch/courseDB.ts +5 -4
  47. package/src/impl/static/courseDB.ts +5 -4
  48. package/src/study/SessionController.ts +58 -10
  49. package/src/study/TagFilteredContentSource.ts +4 -3
  50. package/src/study/services/EloService.ts +22 -3
  51. package/src/study/services/ResponseProcessor.ts +7 -3
@@ -750,13 +750,20 @@ function captureRun(report) {
750
750
  runHistory.pop();
751
751
  }
752
752
  }
753
- function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards) {
753
+ function parseCardElo(provenance) {
754
+ const eloEntry = provenance.find((p) => p.strategy === "elo");
755
+ if (!eloEntry?.reason) return void 0;
756
+ const match = eloEntry.reason.match(/card:\s*(\d+)/);
757
+ return match ? parseInt(match[1], 10) : void 0;
758
+ }
759
+ function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
754
760
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
755
761
  const cards = allCards.map((card) => ({
756
762
  cardId: card.cardId,
757
763
  courseId: card.courseId,
758
764
  origin: getOrigin(card),
759
765
  finalScore: card.score,
766
+ cardElo: parseCardElo(card.provenance),
760
767
  provenance: card.provenance,
761
768
  tags: card.tags,
762
769
  selected: selectedIds.has(card.cardId)
@@ -766,6 +773,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
766
773
  return {
767
774
  courseId,
768
775
  courseName,
776
+ userElo,
769
777
  generatorName,
770
778
  generators,
771
779
  generatedCount,
@@ -786,6 +794,7 @@ function printRunSummary(run) {
786
794
  console.group(`\u{1F50D} Pipeline Run: ${run.courseId} (${run.courseName || "unnamed"})`);
787
795
  logger.info(`Run ID: ${run.runId}`);
788
796
  logger.info(`Time: ${run.timestamp.toISOString()}`);
797
+ logger.info(`User ELO: ${run.userElo ?? "unknown"}`);
789
798
  logger.info(`Generator: ${run.generatorName} \u2192 ${run.generatedCount} candidates`);
790
799
  if (run.generators && run.generators.length > 0) {
791
800
  console.group("Generator breakdown:");
@@ -872,8 +881,12 @@ var init_PipelineDebugger = __esm({
872
881
  console.group(`\u{1F3B4} Card: ${cardId}`);
873
882
  logger.info(`Course: ${card.courseId}`);
874
883
  logger.info(`Origin: ${card.origin}`);
884
+ logger.info(`Card ELO: ${card.cardElo ?? "unknown"} | User ELO: ${run.userElo ?? "unknown"}`);
875
885
  logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
876
886
  logger.info(`Selected: ${card.selected ? "Yes \u2705" : "No \u274C"}`);
887
+ if (card.tags && card.tags.length > 0) {
888
+ logger.info(`Tags (${card.tags.length}): ${card.tags.join(", ")}`);
889
+ }
877
890
  logger.info("Provenance:");
878
891
  logger.info(formatProvenance(card.provenance));
879
892
  console.groupEnd();
@@ -926,6 +939,81 @@ var init_PipelineDebugger = __esm({
926
939
  }
927
940
  console.groupEnd();
928
941
  },
942
+ /**
943
+ * Show prescribed-related cards from the most recent run.
944
+ *
945
+ * Highlights:
946
+ * - cards directly generated by the prescribed strategy
947
+ * - blocked prescribed targets mentioned in provenance
948
+ * - support tags resolved for blocked targets
949
+ *
950
+ * @param groupId - Optional prescribed group ID filter (e.g. 'intro-core')
951
+ */
952
+ showPrescribed(groupId) {
953
+ if (runHistory.length === 0) {
954
+ logger.info("[Pipeline Debug] No runs captured yet.");
955
+ return;
956
+ }
957
+ const run = runHistory[0];
958
+ const prescribedCards = run.cards.filter(
959
+ (c) => c.provenance.some((p) => p.strategy === "prescribed")
960
+ );
961
+ console.group(`\u{1F9ED} Prescribed Debug (${run.courseId})`);
962
+ if (prescribedCards.length === 0) {
963
+ logger.info("No prescribed-generated cards were present in the most recent run.");
964
+ console.groupEnd();
965
+ return;
966
+ }
967
+ const rows = prescribedCards.map((card) => {
968
+ const prescribedProv = card.provenance.find((p) => p.strategy === "prescribed");
969
+ const reason = prescribedProv?.reason ?? "";
970
+ const parsedGroup = reason.match(/group=([^;]+)/)?.[1] ?? "unknown";
971
+ const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
972
+ const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
973
+ const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
974
+ const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
975
+ const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
976
+ return {
977
+ group: parsedGroup,
978
+ mode,
979
+ cardId: card.cardId,
980
+ selected: card.selected ? "yes" : "no",
981
+ finalScore: card.finalScore.toFixed(3),
982
+ blocked,
983
+ blockedTargets,
984
+ supportTags,
985
+ multiplier
986
+ };
987
+ }).filter((row) => !groupId || row.group === groupId).sort((a, b) => Number(b.finalScore) - Number(a.finalScore));
988
+ if (rows.length === 0) {
989
+ logger.info(
990
+ `[Pipeline Debug] No prescribed cards matched group '${groupId}' in the most recent run.`
991
+ );
992
+ console.groupEnd();
993
+ return;
994
+ }
995
+ console.table(rows);
996
+ const selectedRows = rows.filter((r) => r.selected === "yes");
997
+ const blockedTargetSet = /* @__PURE__ */ new Set();
998
+ const supportTagSet = /* @__PURE__ */ new Set();
999
+ for (const row of rows) {
1000
+ if (row.blockedTargets && row.blockedTargets !== "none") {
1001
+ row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
1002
+ }
1003
+ if (row.supportTags && row.supportTags !== "none") {
1004
+ row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
1005
+ }
1006
+ }
1007
+ logger.info(`Prescribed cards in run: ${rows.length}`);
1008
+ logger.info(`Selected prescribed cards: ${selectedRows.length}`);
1009
+ logger.info(
1010
+ `Blocked prescribed targets referenced: ${blockedTargetSet.size > 0 ? [...blockedTargetSet].join(", ") : "none"}`
1011
+ );
1012
+ logger.info(
1013
+ `Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
1014
+ );
1015
+ console.groupEnd();
1016
+ },
929
1017
  /**
930
1018
  * Show all runs in compact format.
931
1019
  */
@@ -1037,6 +1125,27 @@ var init_PipelineDebugger = __esm({
1037
1125
  }
1038
1126
  return _activePipeline.diagnoseCardSpace({ threshold });
1039
1127
  },
1128
+ /**
1129
+ * Show user's per-tag ELO data. Useful for diagnosing hierarchy gate status.
1130
+ *
1131
+ * @param tagFilter - Optional glob pattern(s) to filter tags.
1132
+ * Examples: 'gpc:expose:*', 'gpc:intro:t-T', ['gpc:expose:t-*', 'gpc:intro:t-*']
1133
+ */
1134
+ async showTagElo(tagFilter) {
1135
+ if (!_activePipeline) {
1136
+ logger.info("[Pipeline Debug] No active pipeline. Run a session first.");
1137
+ return;
1138
+ }
1139
+ const status = await _activePipeline.getTagEloStatus(tagFilter);
1140
+ const entries = Object.entries(status).sort(([a], [b]) => a.localeCompare(b));
1141
+ if (entries.length === 0) {
1142
+ logger.info(`[Pipeline Debug] No tag ELO data found${tagFilter ? ` for pattern: ${tagFilter}` : ""}.`);
1143
+ return;
1144
+ }
1145
+ console.table(
1146
+ Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
1147
+ );
1148
+ },
1040
1149
  /**
1041
1150
  * Show help.
1042
1151
  */
@@ -1048,10 +1157,12 @@ Commands:
1048
1157
  .showLastRun() Show summary of most recent pipeline run
1049
1158
  .showRun(id|index) Show summary of a specific run (by index or ID suffix)
1050
1159
  .showCard(cardId) Show provenance trail for a specific card
1160
+ .showTagElo(pattern) Show user's tag ELO data (async). E.g. 'gpc:expose:*'
1051
1161
  .explainReviews() Analyze why reviews were/weren't selected
1052
1162
  .diagnoseCardSpace() Scan full card space through filters (async)
1053
1163
  .showRegistry() Show navigator registry (classes + roles)
1054
1164
  .showStrategies() Show registry + strategy mapping from last run
1165
+ .showPrescribed(id?) Show prescribed-generated cards and blocked/support details from last run
1055
1166
  .listRuns() List all captured runs in table format
1056
1167
  .export() Export run history as JSON for bug reports
1057
1168
  .clear() Clear run history
@@ -1075,6 +1186,44 @@ __export(CompositeGenerator_exports, {
1075
1186
  AggregationMode: () => AggregationMode,
1076
1187
  default: () => CompositeGenerator
1077
1188
  });
1189
+ function mergeHints(allHints) {
1190
+ const defined = allHints.filter((h) => h !== void 0);
1191
+ if (defined.length === 0) return void 0;
1192
+ const merged = {};
1193
+ const boostTags = {};
1194
+ for (const hints of defined) {
1195
+ for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
1196
+ boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
1197
+ }
1198
+ }
1199
+ if (Object.keys(boostTags).length > 0) {
1200
+ merged.boostTags = boostTags;
1201
+ }
1202
+ const boostCards = {};
1203
+ for (const hints of defined) {
1204
+ for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
1205
+ boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
1206
+ }
1207
+ }
1208
+ if (Object.keys(boostCards).length > 0) {
1209
+ merged.boostCards = boostCards;
1210
+ }
1211
+ const concatUnique = (field) => {
1212
+ const values = defined.flatMap((h) => h[field] ?? []);
1213
+ if (values.length > 0) {
1214
+ merged[field] = [...new Set(values)];
1215
+ }
1216
+ };
1217
+ concatUnique("requireTags");
1218
+ concatUnique("requireCards");
1219
+ concatUnique("excludeTags");
1220
+ concatUnique("excludeCards");
1221
+ const labels = defined.map((h) => h._label).filter(Boolean);
1222
+ if (labels.length > 0) {
1223
+ merged._label = labels.join("; ");
1224
+ }
1225
+ return Object.keys(merged).length > 0 ? merged : void 0;
1226
+ }
1078
1227
  var AggregationMode, DEFAULT_AGGREGATION_MODE, FREQUENCY_BOOST_FACTOR, CompositeGenerator;
1079
1228
  var init_CompositeGenerator = __esm({
1080
1229
  "src/core/navigators/generators/CompositeGenerator.ts"() {
@@ -1138,17 +1287,18 @@ var init_CompositeGenerator = __esm({
1138
1287
  this.generators.map((g) => g.getWeightedCards(limit, context))
1139
1288
  );
1140
1289
  const generatorSummaries = [];
1141
- results.forEach((cards, index) => {
1290
+ results.forEach((result, index) => {
1291
+ const cards2 = result.cards;
1142
1292
  const gen = this.generators[index];
1143
1293
  const genName = gen.name || `Generator ${index}`;
1144
- const newCards = cards.filter((c) => c.provenance[0]?.reason?.includes("new card"));
1145
- const reviewCards = cards.filter((c) => c.provenance[0]?.reason?.includes("review"));
1146
- if (cards.length > 0) {
1147
- const topScore = Math.max(...cards.map((c) => c.score)).toFixed(2);
1294
+ const newCards = cards2.filter((c) => c.provenance[0]?.reason?.includes("new card"));
1295
+ const reviewCards = cards2.filter((c) => c.provenance[0]?.reason?.includes("review"));
1296
+ if (cards2.length > 0) {
1297
+ const topScore = Math.max(...cards2.map((c) => c.score)).toFixed(2);
1148
1298
  const parts = [];
1149
1299
  if (newCards.length > 0) parts.push(`${newCards.length} new`);
1150
1300
  if (reviewCards.length > 0) parts.push(`${reviewCards.length} reviews`);
1151
- const breakdown = parts.length > 0 ? parts.join(", ") : `${cards.length} cards`;
1301
+ const breakdown = parts.length > 0 ? parts.join(", ") : `${cards2.length} cards`;
1152
1302
  generatorSummaries.push(`${genName}: ${breakdown} (top: ${topScore})`);
1153
1303
  } else {
1154
1304
  generatorSummaries.push(`${genName}: 0 cards`);
@@ -1156,7 +1306,8 @@ var init_CompositeGenerator = __esm({
1156
1306
  });
1157
1307
  logger.info(`[Composite] Generator breakdown: ${generatorSummaries.join(" | ")}`);
1158
1308
  const byCardId = /* @__PURE__ */ new Map();
1159
- results.forEach((cards, index) => {
1309
+ results.forEach((result, index) => {
1310
+ const cards2 = result.cards;
1160
1311
  const gen = this.generators[index];
1161
1312
  let weight = gen.learnable?.weight ?? 1;
1162
1313
  let deviation;
@@ -1167,7 +1318,7 @@ var init_CompositeGenerator = __esm({
1167
1318
  deviation = context.orchestration.getDeviation(strategyId);
1168
1319
  }
1169
1320
  }
1170
- for (const card of cards) {
1321
+ for (const card of cards2) {
1171
1322
  if (card.provenance.length > 0) {
1172
1323
  card.provenance[0].effectiveWeight = weight;
1173
1324
  card.provenance[0].deviation = deviation;
@@ -1179,15 +1330,15 @@ var init_CompositeGenerator = __esm({
1179
1330
  });
1180
1331
  const merged = [];
1181
1332
  for (const [, items] of byCardId) {
1182
- const cards = items.map((i) => i.card);
1333
+ const cards2 = items.map((i) => i.card);
1183
1334
  const aggregatedScore = this.aggregateScores(items);
1184
1335
  const finalScore = Math.min(1, aggregatedScore);
1185
- const mergedProvenance = cards.flatMap((c) => c.provenance);
1186
- const initialScore = cards[0].score;
1336
+ const mergedProvenance = cards2.flatMap((c) => c.provenance);
1337
+ const initialScore = cards2[0].score;
1187
1338
  const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
1188
1339
  const reason = this.buildAggregationReason(items, finalScore);
1189
1340
  merged.push({
1190
- ...cards[0],
1341
+ ...cards2[0],
1191
1342
  score: finalScore,
1192
1343
  provenance: [
1193
1344
  ...mergedProvenance,
@@ -1202,7 +1353,9 @@ var init_CompositeGenerator = __esm({
1202
1353
  ]
1203
1354
  });
1204
1355
  }
1205
- return merged.sort((a, b) => b.score - a.score).slice(0, limit);
1356
+ const cards = merged.sort((a, b) => b.score - a.score).slice(0, limit);
1357
+ const hints = mergeHints(results.map((result) => result.hints));
1358
+ return { cards, hints };
1206
1359
  }
1207
1360
  /**
1208
1361
  * Build human-readable reason for score aggregation.
@@ -1333,16 +1486,16 @@ var init_elo = __esm({
1333
1486
  };
1334
1487
  });
1335
1488
  scored.sort((a, b) => b.score - a.score);
1336
- const result = scored.slice(0, limit);
1337
- if (result.length > 0) {
1338
- const topScores = result.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ");
1489
+ const cards = scored.slice(0, limit);
1490
+ if (cards.length > 0) {
1491
+ const topScores = cards.slice(0, 3).map((c) => c.score.toFixed(2)).join(", ");
1339
1492
  logger.info(
1340
- `[ELO] Course ${this.course.getCourseID()}: ${result.length} new cards (top scores: ${topScores})`
1493
+ `[ELO] Course ${this.course.getCourseID()}: ${cards.length} new cards (top scores: ${topScores})`
1341
1494
  );
1342
1495
  } else {
1343
1496
  logger.info(`[ELO] Course ${this.course.getCourseID()}: No new cards available`);
1344
1497
  }
1345
- return result;
1498
+ return { cards };
1346
1499
  }
1347
1500
  };
1348
1501
  }
@@ -1361,60 +1514,476 @@ var prescribed_exports = {};
1361
1514
  __export(prescribed_exports, {
1362
1515
  default: () => PrescribedCardsGenerator
1363
1516
  });
1364
- var PrescribedCardsGenerator;
1517
+ function dedupe(arr) {
1518
+ return [...new Set(arr)];
1519
+ }
1520
+ function isoNow() {
1521
+ return (/* @__PURE__ */ new Date()).toISOString();
1522
+ }
1523
+ function clamp(value, min, max) {
1524
+ return Math.max(min, Math.min(max, value));
1525
+ }
1526
+ function matchesTagPattern(tag, pattern) {
1527
+ if (pattern === "*") return true;
1528
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
1529
+ const re = new RegExp(`^${escaped}$`);
1530
+ return re.test(tag);
1531
+ }
1532
+ function pickTopByScore(cards, limit) {
1533
+ return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1534
+ }
1535
+ 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, PRESCRIBED_DEBUG_VERSION, PrescribedCardsGenerator;
1365
1536
  var init_prescribed = __esm({
1366
1537
  "src/core/navigators/generators/prescribed.ts"() {
1367
1538
  "use strict";
1368
1539
  init_navigators();
1369
1540
  init_logger();
1541
+ DEFAULT_FRESHNESS_WINDOW = 3;
1542
+ DEFAULT_MAX_DIRECT_PER_RUN = 3;
1543
+ DEFAULT_MAX_SUPPORT_PER_RUN = 3;
1544
+ DEFAULT_HIERARCHY_DEPTH = 2;
1545
+ DEFAULT_MIN_COUNT = 3;
1546
+ BASE_TARGET_SCORE = 1;
1547
+ BASE_SUPPORT_SCORE = 0.8;
1548
+ MAX_TARGET_MULTIPLIER = 8;
1549
+ MAX_SUPPORT_MULTIPLIER = 4;
1550
+ LOCKED_TAG_PREFIXES = ["concept:"];
1551
+ LESSON_GATE_PENALTY_TAG_HINT = "concept:";
1552
+ PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
1370
1553
  PrescribedCardsGenerator = class extends ContentNavigator {
1371
1554
  name;
1372
1555
  config;
1373
1556
  constructor(user, course, strategyData) {
1374
1557
  super(user, course, strategyData);
1375
1558
  this.name = strategyData.name || "Prescribed Cards";
1376
- try {
1377
- const parsed = JSON.parse(strategyData.serializedData);
1378
- this.config = { cardIds: parsed.cardIds || [] };
1379
- } catch {
1380
- this.config = { cardIds: [] };
1381
- }
1559
+ this.config = this.parseConfig(strategyData.serializedData);
1382
1560
  logger.debug(
1383
- `[Prescribed] Initialized with ${this.config.cardIds.length} prescribed cards`
1561
+ `[Prescribed] Initialized with ${this.config.groups.length} groups and ${this.config.groups.reduce((n, g) => n + g.targetCardIds.length, 0)} targets`
1384
1562
  );
1385
1563
  }
1386
- async getWeightedCards(limit, _context) {
1387
- if (this.config.cardIds.length === 0) {
1388
- return [];
1564
+ get strategyKey() {
1565
+ return "PrescribedProgress";
1566
+ }
1567
+ async getWeightedCards(limit, context) {
1568
+ if (this.config.groups.length === 0 || limit <= 0) {
1569
+ return { cards: [] };
1389
1570
  }
1390
1571
  const courseId = this.course.getCourseID();
1391
1572
  const activeCards = await this.user.getActiveCards();
1392
1573
  const activeIds = new Set(activeCards.map((ac) => ac.cardID));
1393
- const eligibleIds = this.config.cardIds.filter((id) => !activeIds.has(id));
1394
- if (eligibleIds.length === 0) {
1395
- logger.debug("[Prescribed] All prescribed cards already active, returning empty");
1396
- return [];
1397
- }
1398
- const cards = eligibleIds.slice(0, limit).map((cardId) => ({
1399
- cardId,
1400
- courseId,
1401
- score: 1,
1402
- provenance: [
1403
- {
1404
- strategy: "prescribed",
1405
- strategyName: this.strategyName || this.name,
1406
- strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
1407
- action: "generated",
1408
- score: 1,
1409
- reason: `Prescribed card (${eligibleIds.length} eligible of ${this.config.cardIds.length} configured)`
1574
+ const seenCards = await this.user.getSeenCards(courseId).catch(() => []);
1575
+ const seenIds = new Set(seenCards);
1576
+ const progress = await this.getStrategyState() ?? {
1577
+ updatedAt: isoNow(),
1578
+ groups: {}
1579
+ };
1580
+ const hierarchyConfigs = await this.loadHierarchyConfigs();
1581
+ const courseReg = await this.user.getCourseRegDoc(courseId).catch(() => null);
1582
+ const userGlobalElo = typeof courseReg?.elo === "number" ? courseReg.elo : courseReg?.elo?.global?.score ?? context?.userElo ?? 1e3;
1583
+ const userTagElo = typeof courseReg?.elo === "number" ? {} : courseReg?.elo?.tags ?? {};
1584
+ const allTargetIds = dedupe(this.config.groups.flatMap((g) => g.targetCardIds));
1585
+ const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
1586
+ const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
1587
+ const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
1588
+ const nextState = {
1589
+ updatedAt: isoNow(),
1590
+ groups: {}
1591
+ };
1592
+ const emitted = [];
1593
+ const emittedIds = /* @__PURE__ */ new Set();
1594
+ const groupRuntimes = [];
1595
+ for (const group of this.config.groups) {
1596
+ const runtime = this.buildGroupRuntimeState({
1597
+ group,
1598
+ priorState: progress.groups[group.id],
1599
+ activeIds,
1600
+ seenIds,
1601
+ tagsByCard,
1602
+ hierarchyConfigs,
1603
+ userTagElo,
1604
+ userGlobalElo
1605
+ });
1606
+ groupRuntimes.push(runtime);
1607
+ nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
1608
+ const directCards = this.buildDirectTargetCards(
1609
+ runtime,
1610
+ courseId,
1611
+ emittedIds
1612
+ );
1613
+ const supportCards = this.buildSupportCards(
1614
+ runtime,
1615
+ courseId,
1616
+ emittedIds
1617
+ );
1618
+ emitted.push(...directCards, ...supportCards);
1619
+ }
1620
+ const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1621
+ const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
1622
+ boostTags: hintSummary.boostTags,
1623
+ _label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
1624
+ } : void 0;
1625
+ if (emitted.length === 0) {
1626
+ logger.debug("[Prescribed] No prescribed targets/support emitted this run");
1627
+ await this.putStrategyState(nextState).catch((e) => {
1628
+ logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
1629
+ });
1630
+ return hints ? { cards: [], hints } : { cards: [] };
1631
+ }
1632
+ const finalCards = pickTopByScore(emitted, limit);
1633
+ const surfacedByGroup = /* @__PURE__ */ new Map();
1634
+ for (const card of finalCards) {
1635
+ const prov = card.provenance[0];
1636
+ const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
1637
+ const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
1638
+ if (!groupId) continue;
1639
+ if (!surfacedByGroup.has(groupId)) {
1640
+ surfacedByGroup.set(groupId, { targetIds: [], supportIds: [] });
1641
+ }
1642
+ surfacedByGroup.get(groupId)[mode].push(card.cardId);
1643
+ }
1644
+ for (const group of this.config.groups) {
1645
+ const groupState = nextState.groups[group.id];
1646
+ const surfaced = surfacedByGroup.get(group.id);
1647
+ if (surfaced && (surfaced.targetIds.length > 0 || surfaced.supportIds.length > 0)) {
1648
+ groupState.lastSurfacedAt = isoNow();
1649
+ groupState.sessionsSinceSurfaced = 0;
1650
+ if (surfaced.supportIds.length > 0) {
1651
+ groupState.lastSupportAt = isoNow();
1410
1652
  }
1411
- ]
1412
- }));
1653
+ }
1654
+ }
1655
+ await this.putStrategyState(nextState).catch((e) => {
1656
+ logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
1657
+ });
1413
1658
  logger.info(
1414
- `[Prescribed] Emitting ${cards.length} cards (${eligibleIds.length} eligible, ${activeIds.size} already active)`
1659
+ `[Prescribed] Emitting ${finalCards.length} cards (${finalCards.filter((c) => c.provenance[0]?.reason.includes("mode=target")).length} target, ${finalCards.filter((c) => c.provenance[0]?.reason.includes("mode=support")).length} support)`
1415
1660
  );
1661
+ return hints ? { cards: finalCards, hints } : { cards: finalCards };
1662
+ }
1663
+ buildSupportHintSummary(groupRuntimes) {
1664
+ const boostTags = {};
1665
+ const blockedTargetIds = /* @__PURE__ */ new Set();
1666
+ const supportTags = /* @__PURE__ */ new Set();
1667
+ for (const runtime of groupRuntimes) {
1668
+ if (runtime.blockedTargets.length === 0 || runtime.supportTags.length === 0) {
1669
+ continue;
1670
+ }
1671
+ runtime.blockedTargets.forEach((cardId) => blockedTargetIds.add(cardId));
1672
+ for (const tag of runtime.supportTags) {
1673
+ supportTags.add(tag);
1674
+ boostTags[tag] = (boostTags[tag] ?? 1) * runtime.supportMultiplier;
1675
+ }
1676
+ }
1677
+ return {
1678
+ boostTags,
1679
+ blockedTargetIds: [...blockedTargetIds].sort(),
1680
+ supportTags: [...supportTags].sort()
1681
+ };
1682
+ }
1683
+ parseConfig(serializedData) {
1684
+ try {
1685
+ const parsed = JSON.parse(serializedData);
1686
+ const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
1687
+ const groups = groupsRaw.map((raw, i) => ({
1688
+ id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
1689
+ targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
1690
+ supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
1691
+ supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
1692
+ freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
1693
+ maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
1694
+ maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
1695
+ hierarchyWalk: {
1696
+ enabled: raw.hierarchyWalk?.enabled !== false,
1697
+ maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
1698
+ },
1699
+ retireOnEncounter: raw.retireOnEncounter !== false
1700
+ })).filter((g) => g.targetCardIds.length > 0);
1701
+ return { groups };
1702
+ } catch {
1703
+ return { groups: [] };
1704
+ }
1705
+ }
1706
+ async loadHierarchyConfigs() {
1707
+ try {
1708
+ const strategies = await this.course.getNavigationStrategies();
1709
+ return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
1710
+ try {
1711
+ const parsed = JSON.parse(s.serializedData);
1712
+ return {
1713
+ prerequisites: parsed.prerequisites || {}
1714
+ };
1715
+ } catch {
1716
+ return { prerequisites: {} };
1717
+ }
1718
+ });
1719
+ } catch (e) {
1720
+ logger.debug(`[Prescribed] Failed to load hierarchy configs: ${e}`);
1721
+ return [];
1722
+ }
1723
+ }
1724
+ buildGroupRuntimeState(args) {
1725
+ const {
1726
+ group,
1727
+ priorState,
1728
+ activeIds,
1729
+ seenIds,
1730
+ tagsByCard,
1731
+ hierarchyConfigs,
1732
+ userTagElo,
1733
+ userGlobalElo
1734
+ } = args;
1735
+ const encounteredTargets = /* @__PURE__ */ new Set();
1736
+ for (const cardId of group.targetCardIds) {
1737
+ if (activeIds.has(cardId) || seenIds.has(cardId)) {
1738
+ encounteredTargets.add(cardId);
1739
+ }
1740
+ }
1741
+ if (priorState?.encounteredCardIds?.length) {
1742
+ for (const cardId of priorState.encounteredCardIds) {
1743
+ encounteredTargets.add(cardId);
1744
+ }
1745
+ }
1746
+ const pendingTargets = group.targetCardIds.filter((id) => !encounteredTargets.has(id));
1747
+ const targetTags = /* @__PURE__ */ new Map();
1748
+ for (const cardId of pendingTargets) {
1749
+ targetTags.set(cardId, tagsByCard.get(cardId) ?? []);
1750
+ }
1751
+ const blockedTargets = [];
1752
+ const surfaceableTargets = [];
1753
+ const supportTags = /* @__PURE__ */ new Set();
1754
+ for (const cardId of pendingTargets) {
1755
+ const tags = targetTags.get(cardId) ?? [];
1756
+ const resolution = this.resolveBlockedSupportTags(
1757
+ tags,
1758
+ hierarchyConfigs,
1759
+ userTagElo,
1760
+ userGlobalElo,
1761
+ group.hierarchyWalk?.enabled !== false,
1762
+ group.hierarchyWalk?.maxDepth ?? DEFAULT_HIERARCHY_DEPTH
1763
+ );
1764
+ const introTags = tags.filter((tag) => tag.startsWith("gpc:intro:"));
1765
+ const exposeTags = new Set(tags.filter((tag) => tag.startsWith("gpc:expose:")));
1766
+ for (const introTag of introTags) {
1767
+ const suffix = introTag.slice("gpc:intro:".length);
1768
+ if (suffix) {
1769
+ exposeTags.add(`gpc:expose:${suffix}`);
1770
+ }
1771
+ }
1772
+ const unmetExposeTags = [...exposeTags].filter((tag) => {
1773
+ const tagElo = userTagElo[tag];
1774
+ return !tagElo || tagElo.count < DEFAULT_MIN_COUNT;
1775
+ });
1776
+ if (unmetExposeTags.length > 0) {
1777
+ unmetExposeTags.forEach((tag) => supportTags.add(tag));
1778
+ }
1779
+ if (resolution.blocked || unmetExposeTags.length > 0) {
1780
+ blockedTargets.push(cardId);
1781
+ resolution.supportTags.forEach((t) => supportTags.add(t));
1782
+ } else {
1783
+ surfaceableTargets.push(cardId);
1784
+ }
1785
+ }
1786
+ const supportCandidates = dedupe([
1787
+ ...group.supportCardIds ?? [],
1788
+ ...this.findSupportCardsByTags(
1789
+ group,
1790
+ tagsByCard,
1791
+ [...supportTags]
1792
+ )
1793
+ ]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
1794
+ const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
1795
+ const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
1796
+ const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
1797
+ const pressureMultiplier = pendingTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.75 + Math.min(2, pendingTargets.length * 0.1), 1, MAX_TARGET_MULTIPLIER);
1798
+ const supportMultiplier = blockedTargets.length === 0 ? 1 : clamp(1 + staleSessions * 0.5 + Math.min(1.5, blockedTargets.length * 0.15), 1, MAX_SUPPORT_MULTIPLIER);
1799
+ return {
1800
+ group,
1801
+ encounteredTargets,
1802
+ pendingTargets,
1803
+ blockedTargets,
1804
+ surfaceableTargets,
1805
+ targetTags,
1806
+ supportCandidates,
1807
+ supportTags: [...supportTags],
1808
+ pressureMultiplier,
1809
+ supportMultiplier,
1810
+ debugVersion: PRESCRIBED_DEBUG_VERSION
1811
+ };
1812
+ }
1813
+ buildNextGroupState(runtime, prior) {
1814
+ const carriedSessions = prior?.sessionsSinceSurfaced ?? 0;
1815
+ const surfacedThisRun = false;
1816
+ return {
1817
+ encounteredCardIds: [...runtime.encounteredTargets].sort(),
1818
+ pendingTargetIds: [...runtime.pendingTargets].sort(),
1819
+ lastSurfacedAt: prior?.lastSurfacedAt ?? null,
1820
+ sessionsSinceSurfaced: surfacedThisRun ? 0 : carriedSessions + 1,
1821
+ lastSupportAt: prior?.lastSupportAt ?? null,
1822
+ blockedTargetIds: [...runtime.blockedTargets].sort(),
1823
+ lastResolvedSupportTags: [...runtime.supportTags].sort()
1824
+ };
1825
+ }
1826
+ buildDirectTargetCards(runtime, courseId, emittedIds) {
1827
+ const maxDirect = runtime.group.maxDirectTargetsPerRun ?? DEFAULT_MAX_DIRECT_PER_RUN;
1828
+ const directIds = runtime.surfaceableTargets.filter((id) => !emittedIds.has(id)).slice(0, maxDirect);
1829
+ const cards = [];
1830
+ for (const cardId of directIds) {
1831
+ emittedIds.add(cardId);
1832
+ cards.push({
1833
+ cardId,
1834
+ courseId,
1835
+ score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
1836
+ provenance: [
1837
+ {
1838
+ strategy: "prescribed",
1839
+ strategyName: this.strategyName || this.name,
1840
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
1841
+ action: "generated",
1842
+ score: BASE_TARGET_SCORE * runtime.pressureMultiplier,
1843
+ reason: `mode=target;group=${runtime.group.id};pending=${runtime.pendingTargets.length};surfaceable=${runtime.surfaceableTargets.length};blocked=${runtime.blockedTargets.length};blockedTargets=${runtime.blockedTargets.join("|") || "none"};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.pressureMultiplier.toFixed(2)};testversion=${runtime.debugVersion}`
1844
+ }
1845
+ ]
1846
+ });
1847
+ }
1416
1848
  return cards;
1417
1849
  }
1850
+ buildSupportCards(runtime, courseId, emittedIds) {
1851
+ if (runtime.blockedTargets.length === 0 || runtime.supportCandidates.length === 0) {
1852
+ return [];
1853
+ }
1854
+ const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
1855
+ const supportIds = runtime.supportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
1856
+ const cards = [];
1857
+ for (const cardId of supportIds) {
1858
+ emittedIds.add(cardId);
1859
+ cards.push({
1860
+ cardId,
1861
+ courseId,
1862
+ score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
1863
+ provenance: [
1864
+ {
1865
+ strategy: "prescribed",
1866
+ strategyName: this.strategyName || this.name,
1867
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
1868
+ action: "generated",
1869
+ score: BASE_SUPPORT_SCORE * runtime.supportMultiplier,
1870
+ reason: `mode=support;group=${runtime.group.id};pending=${runtime.pendingTargets.length};blocked=${runtime.blockedTargets.length};blockedTargets=${runtime.blockedTargets.join("|") || "none"};supportCard=${cardId};supportTags=${runtime.supportTags.join("|") || "none"};multiplier=${runtime.supportMultiplier.toFixed(2)};testversion=${runtime.debugVersion}`
1871
+ }
1872
+ ]
1873
+ });
1874
+ }
1875
+ return cards;
1876
+ }
1877
+ findSupportCardsByTags(group, tagsByCard, supportTags) {
1878
+ if (supportTags.length === 0) {
1879
+ return [];
1880
+ }
1881
+ const explicitSupportIds = group.supportCardIds ?? [];
1882
+ const explicitPatterns = group.supportTagPatterns ?? [];
1883
+ if (explicitSupportIds.length === 0 && explicitPatterns.length === 0) {
1884
+ return [];
1885
+ }
1886
+ const candidates = /* @__PURE__ */ new Set();
1887
+ for (const cardId of explicitSupportIds) {
1888
+ const cardTags = tagsByCard.get(cardId) ?? [];
1889
+ const matchesResolved = supportTags.some((supportTag) => cardTags.includes(supportTag));
1890
+ const matchesPattern = explicitPatterns.some(
1891
+ (pattern) => cardTags.some((tag) => matchesTagPattern(tag, pattern))
1892
+ );
1893
+ if (matchesResolved || matchesPattern) {
1894
+ candidates.add(cardId);
1895
+ }
1896
+ }
1897
+ return [...candidates];
1898
+ }
1899
+ resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
1900
+ const supportTags = /* @__PURE__ */ new Set();
1901
+ let blocked = false;
1902
+ for (const targetTag of targetTags) {
1903
+ const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[targetTag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
1904
+ if (prereqSets.length === 0) {
1905
+ continue;
1906
+ }
1907
+ const tagBlocked = prereqSets.some(
1908
+ (prereqs) => prereqs.some((pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
1909
+ );
1910
+ if (!tagBlocked) {
1911
+ continue;
1912
+ }
1913
+ blocked = true;
1914
+ if (!hierarchyWalkEnabled) {
1915
+ for (const prereqs of prereqSets) {
1916
+ for (const prereq of prereqs) {
1917
+ if (!this.isPrerequisiteMet(prereq, userTagElo[prereq.tag], userGlobalElo)) {
1918
+ supportTags.add(prereq.tag);
1919
+ }
1920
+ }
1921
+ }
1922
+ continue;
1923
+ }
1924
+ for (const prereqs of prereqSets) {
1925
+ for (const prereq of prereqs) {
1926
+ if (!this.isPrerequisiteMet(prereq, userTagElo[prereq.tag], userGlobalElo)) {
1927
+ this.collectSupportTagsRecursive(
1928
+ prereq.tag,
1929
+ hierarchyConfigs,
1930
+ userTagElo,
1931
+ userGlobalElo,
1932
+ maxDepth,
1933
+ /* @__PURE__ */ new Set(),
1934
+ supportTags
1935
+ );
1936
+ }
1937
+ }
1938
+ }
1939
+ }
1940
+ return { blocked, supportTags: [...supportTags] };
1941
+ }
1942
+ collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
1943
+ if (depth < 0 || visited.has(tag)) return;
1944
+ if (this.isHardGatedTag(tag)) return;
1945
+ visited.add(tag);
1946
+ let walkedFurther = false;
1947
+ for (const hierarchy of hierarchyConfigs) {
1948
+ const prereqs = hierarchy.prerequisites[tag];
1949
+ if (!prereqs || prereqs.length === 0) continue;
1950
+ const unmet = prereqs.filter(
1951
+ (pr) => !this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo)
1952
+ );
1953
+ if (unmet.length > 0 && depth > 0) {
1954
+ walkedFurther = true;
1955
+ for (const prereq of unmet) {
1956
+ this.collectSupportTagsRecursive(
1957
+ prereq.tag,
1958
+ hierarchyConfigs,
1959
+ userTagElo,
1960
+ userGlobalElo,
1961
+ depth - 1,
1962
+ visited,
1963
+ out
1964
+ );
1965
+ }
1966
+ }
1967
+ }
1968
+ if (!walkedFurther) {
1969
+ out.add(tag);
1970
+ }
1971
+ }
1972
+ isHardGatedTag(tag) {
1973
+ return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
1974
+ }
1975
+ isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
1976
+ if (!userTagElo) return false;
1977
+ const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
1978
+ if (userTagElo.count < minCount) return false;
1979
+ if (prereq.masteryThreshold?.minElo !== void 0) {
1980
+ return userTagElo.score >= prereq.masteryThreshold.minElo;
1981
+ }
1982
+ if (prereq.masteryThreshold?.minCount !== void 0) {
1983
+ return true;
1984
+ }
1985
+ return userTagElo.score >= userGlobalElo;
1986
+ }
1418
1987
  };
1419
1988
  }
1420
1989
  });
@@ -1519,7 +2088,7 @@ var init_srs = __esm({
1519
2088
  ]
1520
2089
  };
1521
2090
  });
1522
- return scored.sort((a, b) => b.score - a.score).slice(0, limit);
2091
+ return { cards: scored.sort((a, b) => b.score - a.score).slice(0, limit) };
1523
2092
  }
1524
2093
  /**
1525
2094
  * Compute backlog pressure based on number of due reviews.
@@ -1777,14 +2346,14 @@ var hierarchyDefinition_exports = {};
1777
2346
  __export(hierarchyDefinition_exports, {
1778
2347
  default: () => HierarchyDefinitionNavigator
1779
2348
  });
1780
- var import_common6, DEFAULT_MIN_COUNT, HierarchyDefinitionNavigator;
2349
+ var import_common6, DEFAULT_MIN_COUNT2, HierarchyDefinitionNavigator;
1781
2350
  var init_hierarchyDefinition = __esm({
1782
2351
  "src/core/navigators/filters/hierarchyDefinition.ts"() {
1783
2352
  "use strict";
1784
2353
  init_navigators();
1785
2354
  import_common6 = require("@vue-skuilder/common");
1786
2355
  init_logger();
1787
- DEFAULT_MIN_COUNT = 3;
2356
+ DEFAULT_MIN_COUNT2 = 3;
1788
2357
  HierarchyDefinitionNavigator = class extends ContentNavigator {
1789
2358
  config;
1790
2359
  /** Human-readable name for CardFilter interface */
@@ -1811,7 +2380,7 @@ var init_hierarchyDefinition = __esm({
1811
2380
  */
1812
2381
  isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
1813
2382
  if (!userTagElo) return false;
1814
- const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
2383
+ const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT2;
1815
2384
  if (userTagElo.count < minCount) return false;
1816
2385
  if (prereq.masteryThreshold?.minElo !== void 0) {
1817
2386
  return userTagElo.score >= prereq.masteryThreshold.minElo;
@@ -1912,18 +2481,55 @@ var init_hierarchyDefinition = __esm({
1912
2481
  }
1913
2482
  return boosts;
1914
2483
  }
2484
+ /**
2485
+ * Build a map of gated tag → max configured targetBoost for all *open* gates.
2486
+ *
2487
+ * When a gate opens (prereqs met), cards carrying the gated tag get boosted —
2488
+ * ensuring newly-unlocked content surfaces promptly. The boost is a static
2489
+ * multiplier; natural ELO/SRS deprioritization after interaction handles decay.
2490
+ */
2491
+ getTargetBoosts(unlockedTags) {
2492
+ const boosts = /* @__PURE__ */ new Map();
2493
+ const configKeys = Object.keys(this.config.prerequisites);
2494
+ const unlockedArr = [...unlockedTags];
2495
+ logger.info(
2496
+ `[HierarchyDefinition:targetBoost:trace] ${this.name} | configKeys=${configKeys.length}, unlocked=${unlockedArr.length} (${unlockedArr.slice(0, 5).join(", ")}${unlockedArr.length > 5 ? "..." : ""})`
2497
+ );
2498
+ for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
2499
+ if (!unlockedTags.has(tagId)) continue;
2500
+ logger.info(
2501
+ `[HierarchyDefinition:targetBoost:trace] UNLOCKED ${tagId}: ${prereqs.length} prereqs, raw=${JSON.stringify(prereqs.map((p) => ({ tag: p.tag, tb: p.targetBoost })))}`
2502
+ );
2503
+ for (const prereq of prereqs) {
2504
+ if (!prereq.targetBoost || prereq.targetBoost <= 1) continue;
2505
+ const existing = boosts.get(tagId) ?? 1;
2506
+ boosts.set(tagId, Math.max(existing, prereq.targetBoost));
2507
+ }
2508
+ }
2509
+ if (boosts.size > 0) {
2510
+ logger.info(
2511
+ `[HierarchyDefinition] targetBoosts active: ${[...boosts.entries()].map(([t, b]) => `${t}=\xD7${b}`).join(", ")}`
2512
+ );
2513
+ } else {
2514
+ logger.info(
2515
+ `[HierarchyDefinition:targetBoost:trace] no targetBoosts found despite ${unlockedArr.length} unlocked tags`
2516
+ );
2517
+ }
2518
+ return boosts;
2519
+ }
1915
2520
  /**
1916
2521
  * CardFilter.transform implementation.
1917
2522
  *
1918
- * Two effects:
1919
- * 1. Cards with locked tags receive score * 0.05 (gating penalty)
1920
- * 2. Cards carrying prereq tags of closed gates receive a configured
1921
- * boost (preReqBoost), steering toward content that unlocks gates
2523
+ * Three effects:
2524
+ * 1. Cards with locked tags receive score * 0.02 (gating penalty)
2525
+ * 2. Cards carrying prereq tags of closed gates receive preReqBoost
2526
+ * 3. Cards carrying gated tags of open gates receive targetBoost
1922
2527
  */
1923
2528
  async transform(cards, context) {
1924
2529
  const masteredTags = await this.getMasteredTags(context);
1925
2530
  const unlockedTags = this.getUnlockedTags(masteredTags);
1926
2531
  const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
2532
+ const targetBoosts = this.getTargetBoosts(unlockedTags);
1927
2533
  const gated = [];
1928
2534
  for (const card of cards) {
1929
2535
  const { isUnlocked, reason } = await this.checkCardUnlock(
@@ -1956,6 +2562,26 @@ var init_hierarchyDefinition = __esm({
1956
2562
  );
1957
2563
  }
1958
2564
  }
2565
+ if (isUnlocked && targetBoosts.size > 0) {
2566
+ const cardTags = card.tags ?? [];
2567
+ let maxTargetBoost = 1;
2568
+ const boostedTargets = [];
2569
+ for (const tag of cardTags) {
2570
+ const boost = targetBoosts.get(tag);
2571
+ if (boost && boost > maxTargetBoost) {
2572
+ maxTargetBoost = boost;
2573
+ boostedTargets.push(tag);
2574
+ }
2575
+ }
2576
+ if (maxTargetBoost > 1) {
2577
+ finalScore *= maxTargetBoost;
2578
+ action = "boosted";
2579
+ finalReason = `${finalReason} | targetBoost \xD7${maxTargetBoost.toFixed(2)} for ${boostedTargets.join(", ")}`;
2580
+ logger.info(
2581
+ `[HierarchyDefinition] targetBoost \xD7${maxTargetBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedTargets.join(", ")}] (score: ${card.score.toFixed(3)} \u2192 ${finalScore.toFixed(3)})`
2582
+ );
2583
+ }
2584
+ }
1959
2585
  gated.push({
1960
2586
  ...card,
1961
2587
  score: finalScore,
@@ -2140,13 +2766,13 @@ var interferenceMitigator_exports = {};
2140
2766
  __export(interferenceMitigator_exports, {
2141
2767
  default: () => InterferenceMitigatorNavigator
2142
2768
  });
2143
- var import_common7, DEFAULT_MIN_COUNT2, DEFAULT_MIN_ELAPSED_DAYS, DEFAULT_INTERFERENCE_DECAY, InterferenceMitigatorNavigator;
2769
+ var import_common7, DEFAULT_MIN_COUNT3, DEFAULT_MIN_ELAPSED_DAYS, DEFAULT_INTERFERENCE_DECAY, InterferenceMitigatorNavigator;
2144
2770
  var init_interferenceMitigator = __esm({
2145
2771
  "src/core/navigators/filters/interferenceMitigator.ts"() {
2146
2772
  "use strict";
2147
2773
  init_navigators();
2148
2774
  import_common7 = require("@vue-skuilder/common");
2149
- DEFAULT_MIN_COUNT2 = 10;
2775
+ DEFAULT_MIN_COUNT3 = 10;
2150
2776
  DEFAULT_MIN_ELAPSED_DAYS = 3;
2151
2777
  DEFAULT_INTERFERENCE_DECAY = 0.8;
2152
2778
  InterferenceMitigatorNavigator = class extends ContentNavigator {
@@ -2171,7 +2797,7 @@ var init_interferenceMitigator = __esm({
2171
2797
  return {
2172
2798
  interferenceSets: sets,
2173
2799
  maturityThreshold: {
2174
- minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT2,
2800
+ minCount: parsed.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3,
2175
2801
  minElo: parsed.maturityThreshold?.minElo,
2176
2802
  minElapsedDays: parsed.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS
2177
2803
  },
@@ -2181,7 +2807,7 @@ var init_interferenceMitigator = __esm({
2181
2807
  return {
2182
2808
  interferenceSets: [],
2183
2809
  maturityThreshold: {
2184
- minCount: DEFAULT_MIN_COUNT2,
2810
+ minCount: DEFAULT_MIN_COUNT3,
2185
2811
  minElapsedDays: DEFAULT_MIN_ELAPSED_DAYS
2186
2812
  },
2187
2813
  defaultDecay: DEFAULT_INTERFERENCE_DECAY
@@ -2228,7 +2854,7 @@ var init_interferenceMitigator = __esm({
2228
2854
  try {
2229
2855
  const courseReg = await context.user.getCourseRegDoc(context.course.getCourseID());
2230
2856
  const userElo = (0, import_common7.toCourseElo)(courseReg.elo);
2231
- const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT2;
2857
+ const minCount = this.config.maturityThreshold?.minCount ?? DEFAULT_MIN_COUNT3;
2232
2858
  const minElo = this.config.maturityThreshold?.minElo;
2233
2859
  const minElapsedDays = this.config.maturityThreshold?.minElapsedDays ?? DEFAULT_MIN_ELAPSED_DAYS;
2234
2860
  const minCountForElapsed = minElapsedDays * 2;
@@ -2913,6 +3539,44 @@ function globMatch(value, pattern) {
2913
3539
  function cardMatchesTagPattern(card, pattern) {
2914
3540
  return (card.tags ?? []).some((tag) => globMatch(tag, pattern));
2915
3541
  }
3542
+ function mergeHints2(allHints) {
3543
+ const defined = allHints.filter((h) => h !== null && h !== void 0);
3544
+ if (defined.length === 0) return void 0;
3545
+ const merged = {};
3546
+ const boostTags = {};
3547
+ for (const hints of defined) {
3548
+ for (const [pattern, factor] of Object.entries(hints.boostTags ?? {})) {
3549
+ boostTags[pattern] = (boostTags[pattern] ?? 1) * factor;
3550
+ }
3551
+ }
3552
+ if (Object.keys(boostTags).length > 0) {
3553
+ merged.boostTags = boostTags;
3554
+ }
3555
+ const boostCards = {};
3556
+ for (const hints of defined) {
3557
+ for (const [pattern, factor] of Object.entries(hints.boostCards ?? {})) {
3558
+ boostCards[pattern] = (boostCards[pattern] ?? 1) * factor;
3559
+ }
3560
+ }
3561
+ if (Object.keys(boostCards).length > 0) {
3562
+ merged.boostCards = boostCards;
3563
+ }
3564
+ const concatUnique = (field) => {
3565
+ const values = defined.flatMap((h) => h[field] ?? []);
3566
+ if (values.length > 0) {
3567
+ merged[field] = [...new Set(values)];
3568
+ }
3569
+ };
3570
+ concatUnique("requireTags");
3571
+ concatUnique("requireCards");
3572
+ concatUnique("excludeTags");
3573
+ concatUnique("excludeCards");
3574
+ const labels = defined.map((h) => h._label).filter(Boolean);
3575
+ if (labels.length > 0) {
3576
+ merged._label = labels.join("; ");
3577
+ }
3578
+ return Object.keys(merged).length > 0 ? merged : void 0;
3579
+ }
2916
3580
  function logPipelineConfig(generator, filters) {
2917
3581
  const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
2918
3582
  logger.info(
@@ -3064,9 +3728,12 @@ var init_Pipeline = __esm({
3064
3728
  logger.debug(
3065
3729
  `[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
3066
3730
  );
3067
- let cards = await this.generator.getWeightedCards(fetchLimit, context);
3731
+ const generatorResult = await this.generator.getWeightedCards(fetchLimit, context);
3732
+ let cards = generatorResult.cards;
3068
3733
  const tGenerate = performance.now();
3069
3734
  const generatedCount = cards.length;
3735
+ const mergedHints = mergeHints2([this._ephemeralHints, generatorResult.hints]);
3736
+ this._ephemeralHints = mergedHints ?? null;
3070
3737
  let generatorSummaries;
3071
3738
  if (this.generator.generators) {
3072
3739
  const genMap = /* @__PURE__ */ new Map();
@@ -3144,14 +3811,15 @@ var init_Pipeline = __esm({
3144
3811
  generatorSummaries,
3145
3812
  generatedCount,
3146
3813
  filterImpacts,
3147
- allCardsBeforeFiltering,
3148
- result
3814
+ cards,
3815
+ result,
3816
+ context.userElo
3149
3817
  );
3150
3818
  captureRun(report);
3151
3819
  } catch (e) {
3152
3820
  logger.debug(`[Pipeline] Failed to capture debug run: ${e}`);
3153
3821
  }
3154
- return result;
3822
+ return { cards: result };
3155
3823
  }
3156
3824
  /**
3157
3825
  * Batch hydrate tags for all cards.
@@ -3359,6 +4027,34 @@ var init_Pipeline = __esm({
3359
4027
  return [...new Set(ids)];
3360
4028
  }
3361
4029
  // ---------------------------------------------------------------------------
4030
+ // Tag ELO diagnostic
4031
+ // ---------------------------------------------------------------------------
4032
+ /**
4033
+ * Get the user's per-tag ELO data for specified tags (or all tags).
4034
+ * Useful for diagnosing why hierarchy gates are open/closed.
4035
+ */
4036
+ async getTagEloStatus(tagFilter) {
4037
+ const courseReg = await this.user.getCourseRegDoc(this.course.getCourseID());
4038
+ const courseElo = (0, import_common8.toCourseElo)(courseReg.elo);
4039
+ const result = {};
4040
+ if (!tagFilter) {
4041
+ for (const [tag, data] of Object.entries(courseElo.tags)) {
4042
+ result[tag] = { score: data.score, count: data.count };
4043
+ }
4044
+ } else {
4045
+ const patterns = Array.isArray(tagFilter) ? tagFilter : [tagFilter];
4046
+ for (const pattern of patterns) {
4047
+ const regex = globToRegex(pattern);
4048
+ for (const [tag, data] of Object.entries(courseElo.tags)) {
4049
+ if (regex.test(tag)) {
4050
+ result[tag] = { score: data.score, count: data.count };
4051
+ }
4052
+ }
4053
+ }
4054
+ }
4055
+ return result;
4056
+ }
4057
+ // ---------------------------------------------------------------------------
3362
4058
  // Card-space diagnostic
3363
4059
  // ---------------------------------------------------------------------------
3364
4060
  /**
@@ -4750,7 +5446,7 @@ var init_classroomDB2 = __esm({
4750
5446
  for (const content of due) {
4751
5447
  if (content.type === "course") {
4752
5448
  const db = new CourseDB(content.courseID, async () => this._user);
4753
- const courseCards = await db.getWeightedCards(limit);
5449
+ const { cards: courseCards } = await db.getWeightedCards(limit);
4754
5450
  for (const card of courseCards) {
4755
5451
  if (!activeCardIds.has(card.cardId)) {
4756
5452
  weighted.push({
@@ -4813,7 +5509,7 @@ var init_classroomDB2 = __esm({
4813
5509
  logger.info(
4814
5510
  `[StudentClassroomDB] New cards from classroom ${this._cfg.name}: ${weighted.length} total (reviews + new)`
4815
5511
  );
4816
- return weighted.sort((a, b) => b.score - a.score).slice(0, limit);
5512
+ return { cards: weighted.sort((a, b) => b.score - a.score).slice(0, limit) };
4817
5513
  }
4818
5514
  };
4819
5515
  }
@@ -6049,7 +6745,7 @@ var init_TagFilteredContentSource = __esm({
6049
6745
  async getWeightedCards(limit) {
6050
6746
  if (!(0, import_common14.hasActiveFilter)(this.filter)) {
6051
6747
  logger.warn("[TagFilteredContentSource] getWeightedCards called with no active filter");
6052
- return [];
6748
+ return { cards: [] };
6053
6749
  }
6054
6750
  const eligibleCardIds = await this.resolveFilteredCardIds();
6055
6751
  const activeCards = await this.user.getActiveCards();
@@ -6101,7 +6797,7 @@ var init_TagFilteredContentSource = __esm({
6101
6797
  }
6102
6798
  ]
6103
6799
  }));
6104
- return [...reviewWeighted, ...newCardWeighted].slice(0, limit);
6800
+ return { cards: [...reviewWeighted, ...newCardWeighted].slice(0, limit) };
6105
6801
  }
6106
6802
  /**
6107
6803
  * Clears the cached resolved card IDs.