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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -888,8 +888,9 @@ function getOrigin(card) {
888
888
  const firstEntry = card.provenance[0];
889
889
  if (!firstEntry) return "unknown";
890
890
  const reason = firstEntry.reason?.toLowerCase() || "";
891
- if (reason.includes("new card")) return "new";
892
- if (reason.includes("review")) return "review";
891
+ const strategy = firstEntry.strategy?.toLowerCase() || "";
892
+ if (reason.includes("new card") || strategy.includes("elo")) return "new";
893
+ if (reason.includes("review") || strategy.includes("srs")) return "review";
893
894
  return "unknown";
894
895
  }
895
896
  function captureRun(report) {
@@ -909,12 +910,13 @@ function parseCardElo(provenance) {
909
910
  const match = eloEntry.reason.match(/card:\s*(\d+)/);
910
911
  return match ? parseInt(match[1], 10) : void 0;
911
912
  }
912
- function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
913
+ function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
913
914
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
914
915
  const cards = allCards.map((card) => ({
915
916
  cardId: card.cardId,
916
917
  courseId: card.courseId,
917
918
  origin: getOrigin(card),
919
+ generator: card.provenance[0]?.strategyName || card.provenance[0]?.strategy,
918
920
  finalScore: card.score,
919
921
  cardElo: parseCardElo(card.provenance),
920
922
  provenance: card.provenance,
@@ -931,6 +933,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
931
933
  generators,
932
934
  generatedCount,
933
935
  filters,
936
+ hints,
934
937
  finalCount: selectedCards.length,
935
938
  reviewsSelected,
936
939
  newSelected,
@@ -970,13 +973,169 @@ function printRunSummary(run) {
970
973
  );
971
974
  console.groupEnd();
972
975
  }
976
+ function renderUI() {
977
+ if (!_uiContainer) return;
978
+ const runs = runHistory;
979
+ const selectedRun = _selectedRunIndex !== null ? runs[_selectedRunIndex] : null;
980
+ const styles = `
981
+ #sk-pipeline-debugger {
982
+ position: fixed;
983
+ top: 0;
984
+ left: 0;
985
+ width: 100vw;
986
+ height: 100vh;
987
+ background: #f8f9fa;
988
+ color: #212529;
989
+ z-index: 999999;
990
+ display: flex;
991
+ flex-direction: column;
992
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
993
+ font-size: 14px;
994
+ }
995
+ #sk-pipeline-debugger header {
996
+ padding: 1rem;
997
+ background: #343a40;
998
+ color: white;
999
+ display: flex;
1000
+ justify-content: space-between;
1001
+ align-items: center;
1002
+ }
1003
+ #sk-pipeline-debugger .container {
1004
+ display: flex;
1005
+ flex: 1;
1006
+ overflow: hidden;
1007
+ }
1008
+ #sk-pipeline-debugger .sidebar {
1009
+ width: 300px;
1010
+ border-right: 1px solid #dee2e6;
1011
+ overflow-y: auto;
1012
+ background: white;
1013
+ }
1014
+ #sk-pipeline-debugger .main-content {
1015
+ flex: 1;
1016
+ overflow-y: auto;
1017
+ padding: 1.5rem;
1018
+ }
1019
+ #sk-pipeline-debugger .run-item {
1020
+ padding: 0.75rem 1rem;
1021
+ border-bottom: 1px solid #eee;
1022
+ cursor: pointer;
1023
+ }
1024
+ #sk-pipeline-debugger .run-item:hover { background: #f1f3f5; }
1025
+ #sk-pipeline-debugger .run-item.active { background: #e9ecef; border-left: 4px solid #007bff; }
1026
+ #sk-pipeline-debugger h2, #sk-pipeline-debugger h3 { margin-top: 0; }
1027
+ #sk-pipeline-debugger table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; background: white; }
1028
+ #sk-pipeline-debugger th, #sk-pipeline-debugger td { border: 1px solid #dee2e6; padding: 0.5rem; text-align: left; }
1029
+ #sk-pipeline-debugger th { background: #f1f3f5; }
1030
+ #sk-pipeline-debugger code { background: #f1f3f5; padding: 0.1rem 0.3rem; border-radius: 3px; font-family: monospace; }
1031
+ #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
1032
+ #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
1033
+ #sk-pipeline-debugger .provenance { font-size: 12px; color: #666; margin-top: 0.25rem; white-space: pre-wrap; font-family: monospace; background: #f8f9fa; padding: 0.5rem; border-radius: 4px; }
1034
+ `;
1035
+ const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
1036
+ (r, i) => `
1037
+ <div class="run-item ${i === _selectedRunIndex ? "active" : ""}" onclick="window.skuilder.pipeline._selectRun(${i})">
1038
+ <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
1039
+ <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
1040
+ <small>${r.finalCount} cards selected</small>
1041
+ </div>
1042
+ `
1043
+ ).join("");
1044
+ let detailsHtml = '<div style="color: #6c757d; text-align: center; margin-top: 5rem;">Select a run to see details</div>';
1045
+ if (selectedRun) {
1046
+ const filteredCards = selectedRun.cards.filter(
1047
+ (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
1048
+ );
1049
+ detailsHtml = `
1050
+ <h2>Run: ${selectedRun.runId}</h2>
1051
+ <p>
1052
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
1053
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
1054
+ <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
1055
+ </p>
1056
+
1057
+ <h3>Pipeline Config</h3>
1058
+ <table>
1059
+ <tr><th>Generator</th><td>${selectedRun.generatorName} (${selectedRun.generatedCount} candidates)</td></tr>
1060
+ ${(selectedRun.generators || []).map(
1061
+ (g) => `
1062
+ <tr><td style="padding-left: 2rem;">\u21B3 ${g.name}</td><td>${g.cardCount} cards (${g.newCount} new, ${g.reviewCount} review, top: ${g.topScore.toFixed(2)})</td></tr>
1063
+ `
1064
+ ).join("")}
1065
+ </table>
1066
+
1067
+ ${selectedRun.hints ? `
1068
+ <h3>Ephemeral Hints</h3>
1069
+ <table>
1070
+ ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
1071
+ ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
1072
+ ${selectedRun.hints.boostCards ? `<tr><th>Boost Cards</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostCards, null, 2)}</pre></td></tr>` : ""}
1073
+ ${selectedRun.hints.requireTags ? `<tr><th>Require Tags</th><td>${selectedRun.hints.requireTags.join(", ")}</td></tr>` : ""}
1074
+ ${selectedRun.hints.requireCards ? `<tr><th>Require Cards</th><td>${selectedRun.hints.requireCards.join(", ")}</td></tr>` : ""}
1075
+ ${selectedRun.hints.excludeTags ? `<tr><th>Exclude Tags</th><td>${selectedRun.hints.excludeTags.join(", ")}</td></tr>` : ""}
1076
+ ${selectedRun.hints.excludeCards ? `<tr><th>Exclude Cards</th><td>${selectedRun.hints.excludeCards.join(", ")}</td></tr>` : ""}
1077
+ </table>
1078
+ ` : ""}
1079
+
1080
+ <h3>Filter Impact</h3>
1081
+ <table>
1082
+ <thead><tr><th>Filter</th><th>Boosted</th><th>Penalized</th><th>Passed</th><th>Removed</th></tr></thead>
1083
+ <tbody>
1084
+ ${selectedRun.filters.map(
1085
+ (f) => `
1086
+ <tr><td>${f.name}</td><td>\u2191${f.boosted}</td><td>\u2193${f.penalized}</td><td>=${f.passed}</td><td>\u2715${f.removed}</td></tr>
1087
+ `
1088
+ ).join("")}
1089
+ </tbody>
1090
+ </table>
1091
+
1092
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
1093
+ <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
1094
+
1095
+ <table>
1096
+ <thead><tr><th>ID</th><th>Generator</th><th>Origin</th><th>Score</th><th>Selected</th></tr></thead>
1097
+ <tbody>
1098
+ ${filteredCards.map(
1099
+ (c) => `
1100
+ <tr>
1101
+ <td><code>${c.cardId}</code></td>
1102
+ <td>${c.generator || "unknown"}</td>
1103
+ <td>${c.origin}</td>
1104
+ <td>${c.finalScore.toFixed(3)}</td>
1105
+ <td>${c.selected ? "\u2705" : "\u274C"}</td>
1106
+ </tr>
1107
+ ${c.selected || _cardSearchQuery ? `
1108
+ <tr>
1109
+ <td colspan="5">
1110
+ <div class="provenance">${formatProvenance(c.provenance)}</div>
1111
+ </td>
1112
+ </tr>
1113
+ ` : ""}
1114
+ `
1115
+ ).join("")}
1116
+ </tbody>
1117
+ </table>
1118
+ `;
1119
+ }
1120
+ _uiContainer.innerHTML = `
1121
+ <style>${styles}</style>
1122
+ <header>
1123
+ <strong>Pipeline Debugger</strong>
1124
+ <button class="close-btn" onclick="window.skuilder.pipeline.ui()">Close</button>
1125
+ </header>
1126
+ <div class="container">
1127
+ <div class="sidebar">${runListHtml}</div>
1128
+ <div class="main-content">${detailsHtml}</div>
1129
+ </div>
1130
+ `;
1131
+ }
973
1132
  function mountPipelineDebugger() {
974
1133
  if (typeof window === "undefined") return;
975
1134
  const win = window;
976
1135
  win.skuilder = win.skuilder || {};
977
1136
  win.skuilder.pipeline = pipelineDebugAPI;
978
1137
  }
979
- var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
1138
+ var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
980
1139
  var init_PipelineDebugger = __esm({
981
1140
  "src/core/navigators/PipelineDebugger.ts"() {
982
1141
  "use strict";
@@ -985,6 +1144,9 @@ var init_PipelineDebugger = __esm({
985
1144
  _activePipeline = null;
986
1145
  MAX_RUNS = 10;
987
1146
  runHistory = [];
1147
+ _uiContainer = null;
1148
+ _selectedRunIndex = null;
1149
+ _cardSearchQuery = "";
988
1150
  pipelineDebugAPI = {
989
1151
  /**
990
1152
  * Get raw run history for programmatic access.
@@ -1124,16 +1286,20 @@ var init_PipelineDebugger = __esm({
1124
1286
  const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
1125
1287
  const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
1126
1288
  const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
1289
+ const supportCard = reason.match(/supportCard=([^;]+)/)?.[1] ?? "none";
1127
1290
  const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
1128
1291
  const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
1292
+ const supportSource = mode === "discovered-support" ? "discovered" : mode === "support" ? "authored" : "n/a";
1129
1293
  return {
1130
1294
  group: parsedGroup,
1131
1295
  mode,
1296
+ supportSource,
1132
1297
  cardId: card.cardId,
1133
1298
  selected: card.selected ? "yes" : "no",
1134
1299
  finalScore: card.finalScore.toFixed(3),
1135
1300
  blocked,
1136
1301
  blockedTargets,
1302
+ supportCard,
1137
1303
  supportTags,
1138
1304
  multiplier
1139
1305
  };
@@ -1149,6 +1315,8 @@ var init_PipelineDebugger = __esm({
1149
1315
  const selectedRows = rows.filter((r) => r.selected === "yes");
1150
1316
  const blockedTargetSet = /* @__PURE__ */ new Set();
1151
1317
  const supportTagSet = /* @__PURE__ */ new Set();
1318
+ const authoredSupportSet = /* @__PURE__ */ new Set();
1319
+ const discoveredSupportSet = /* @__PURE__ */ new Set();
1152
1320
  for (const row of rows) {
1153
1321
  if (row.blockedTargets && row.blockedTargets !== "none") {
1154
1322
  row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
@@ -1156,6 +1324,13 @@ var init_PipelineDebugger = __esm({
1156
1324
  if (row.supportTags && row.supportTags !== "none") {
1157
1325
  row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
1158
1326
  }
1327
+ if (row.supportCard && row.supportCard !== "none") {
1328
+ if (row.supportSource === "discovered") {
1329
+ discoveredSupportSet.add(row.supportCard);
1330
+ } else if (row.supportSource === "authored") {
1331
+ authoredSupportSet.add(row.supportCard);
1332
+ }
1333
+ }
1159
1334
  }
1160
1335
  logger.info(`Prescribed cards in run: ${rows.length}`);
1161
1336
  logger.info(`Selected prescribed cards: ${selectedRows.length}`);
@@ -1165,6 +1340,12 @@ var init_PipelineDebugger = __esm({
1165
1340
  logger.info(
1166
1341
  `Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
1167
1342
  );
1343
+ logger.info(
1344
+ `Authored support cards emitted: ${authoredSupportSet.size > 0 ? [...authoredSupportSet].join(", ") : "none"}`
1345
+ );
1346
+ logger.info(
1347
+ `Discovered support cards emitted: ${discoveredSupportSet.size > 0 ? [...discoveredSupportSet].join(", ") : "none"}`
1348
+ );
1168
1349
  console.groupEnd();
1169
1350
  },
1170
1351
  /**
@@ -1299,6 +1480,39 @@ var init_PipelineDebugger = __esm({
1299
1480
  Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
1300
1481
  );
1301
1482
  },
1483
+ /**
1484
+ * Toggle the full-screen UI debugger.
1485
+ */
1486
+ ui() {
1487
+ if (_uiContainer) {
1488
+ document.body.removeChild(_uiContainer);
1489
+ _uiContainer = null;
1490
+ return;
1491
+ }
1492
+ _uiContainer = document.createElement("div");
1493
+ _uiContainer.id = "sk-pipeline-debugger";
1494
+ document.body.appendChild(_uiContainer);
1495
+ if (_selectedRunIndex === null && runHistory.length > 0) {
1496
+ _selectedRunIndex = 0;
1497
+ }
1498
+ renderUI();
1499
+ },
1500
+ /**
1501
+ * Internal UI helpers
1502
+ * @internal
1503
+ */
1504
+ _selectRun(index) {
1505
+ _selectedRunIndex = index;
1506
+ renderUI();
1507
+ },
1508
+ /**
1509
+ * Internal UI helpers
1510
+ * @internal
1511
+ */
1512
+ _setSearch(query) {
1513
+ _cardSearchQuery = query;
1514
+ renderUI();
1515
+ },
1302
1516
  /**
1303
1517
  * Show help.
1304
1518
  */
@@ -1307,6 +1521,7 @@ var init_PipelineDebugger = __esm({
1307
1521
  \u{1F527} Pipeline Debug API
1308
1522
 
1309
1523
  Commands:
1524
+ .ui() Toggle full-screen UI debugger
1310
1525
  .showLastRun() Show summary of most recent pipeline run
1311
1526
  .showRun(id|index) Show summary of a specific run (by index or ID suffix)
1312
1527
  .showCard(cardId) Show provenance trail for a specific card
@@ -1323,6 +1538,7 @@ Commands:
1323
1538
  .help() Show this help message
1324
1539
 
1325
1540
  Example:
1541
+ window.skuilder.pipeline.ui()
1326
1542
  window.skuilder.pipeline.showLastRun()
1327
1543
  window.skuilder.pipeline.showRun(1)
1328
1544
  await window.skuilder.pipeline.diagnoseCardSpace()
@@ -1485,7 +1701,7 @@ var init_CompositeGenerator = __esm({
1485
1701
  for (const [, items] of byCardId) {
1486
1702
  const cards2 = items.map((i) => i.card);
1487
1703
  const aggregatedScore = this.aggregateScores(items);
1488
- const finalScore = Math.min(1, aggregatedScore);
1704
+ const finalScore = Math.max(0, aggregatedScore);
1489
1705
  const mergedProvenance = cards2.flatMap((c) => c.provenance);
1490
1706
  const initialScore = cards2[0].score;
1491
1707
  const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
@@ -1682,10 +1898,26 @@ function matchesTagPattern(tag, pattern) {
1682
1898
  const re = new RegExp(`^${escaped}$`);
1683
1899
  return re.test(tag);
1684
1900
  }
1901
+ function extractWordStem(cardId) {
1902
+ for (const prefix of ["c-ml-", "c-ws-", "c-spelling-"]) {
1903
+ if (cardId.startsWith(prefix)) {
1904
+ const rest = cardId.slice(prefix.length);
1905
+ const lastDash = rest.lastIndexOf("-");
1906
+ return lastDash > 0 ? rest.slice(0, lastDash) : rest;
1907
+ }
1908
+ }
1909
+ return cardId;
1910
+ }
1911
+ function shuffleInPlace(arr) {
1912
+ for (let i = arr.length - 1; i > 0; i--) {
1913
+ const j = Math.floor(Math.random() * (i + 1));
1914
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1915
+ }
1916
+ }
1685
1917
  function pickTopByScore(cards, limit) {
1686
1918
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1687
1919
  }
1688
- 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;
1920
+ 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, DISCOVERED_SUPPORT_SCORE, MAX_TARGET_MULTIPLIER, MAX_SUPPORT_MULTIPLIER, PRESCRIBED_DEBUG_VERSION, PrescribedCardsGenerator;
1689
1921
  var init_prescribed = __esm({
1690
1922
  "src/core/navigators/generators/prescribed.ts"() {
1691
1923
  "use strict";
@@ -1698,11 +1930,10 @@ var init_prescribed = __esm({
1698
1930
  DEFAULT_MIN_COUNT = 3;
1699
1931
  BASE_TARGET_SCORE = 1;
1700
1932
  BASE_SUPPORT_SCORE = 0.8;
1933
+ DISCOVERED_SUPPORT_SCORE = 12;
1701
1934
  MAX_TARGET_MULTIPLIER = 8;
1702
1935
  MAX_SUPPORT_MULTIPLIER = 4;
1703
- LOCKED_TAG_PREFIXES = ["concept:"];
1704
- LESSON_GATE_PENALTY_TAG_HINT = "concept:";
1705
- PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
1936
+ PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
1706
1937
  PrescribedCardsGenerator = class extends ContentNavigator {
1707
1938
  name;
1708
1939
  config;
@@ -1738,6 +1969,20 @@ var init_prescribed = __esm({
1738
1969
  const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
1739
1970
  const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
1740
1971
  const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
1972
+ const courseTagDocs = await this.course.getCourseTagStubs().catch(
1973
+ () => ({
1974
+ rows: [],
1975
+ offset: 0,
1976
+ total_rows: 0
1977
+ })
1978
+ );
1979
+ const cardsByTag = /* @__PURE__ */ new Map();
1980
+ for (const row of courseTagDocs.rows ?? []) {
1981
+ const tagDoc = row.doc;
1982
+ if (tagDoc?.name && Array.isArray(tagDoc.taggedCards)) {
1983
+ cardsByTag.set(tagDoc.name, [...tagDoc.taggedCards]);
1984
+ }
1985
+ }
1741
1986
  const nextState = {
1742
1987
  updatedAt: isoNow(),
1743
1988
  groups: {}
@@ -1752,11 +1997,31 @@ var init_prescribed = __esm({
1752
1997
  activeIds,
1753
1998
  seenIds,
1754
1999
  tagsByCard,
2000
+ cardsByTag,
1755
2001
  hierarchyConfigs,
1756
2002
  userTagElo,
1757
2003
  userGlobalElo
1758
2004
  });
1759
2005
  groupRuntimes.push(runtime);
2006
+ logger.info(
2007
+ `[Prescribed] Group '${group.id}': ${group.targetCardIds.length} targets total, ${runtime.encounteredTargets.size} encountered, ${runtime.pendingTargets.length} pending (${runtime.surfaceableTargets.length} surfaceable, ${runtime.blockedTargets.length} blocked), ${runtime.supportCandidates.length} authored support candidates, ${runtime.discoveredSupportCandidates.length} discovered support candidates, pressure=${runtime.pressureMultiplier.toFixed(2)}`
2008
+ );
2009
+ if (runtime.blockedTargets.length > 0) {
2010
+ logger.info(
2011
+ `[Prescribed] Group '${group.id}' blocked targets: ${runtime.blockedTargets.join(", ")}`
2012
+ );
2013
+ logger.info(
2014
+ `[Prescribed] Group '${group.id}' support tags needed: ${runtime.supportTags.join(", ") || "(none)"}`
2015
+ );
2016
+ logger.info(
2017
+ `[Prescribed] Group '${group.id}' escalation mode: ` + (runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support-candidates" : "boost-only")
2018
+ );
2019
+ if (runtime.discoveredSupportCandidates.length > 0) {
2020
+ logger.info(
2021
+ `[Prescribed] Group '${group.id}' discovered support candidates: ${runtime.discoveredSupportCandidates.join(", ")}`
2022
+ );
2023
+ }
2024
+ }
1760
2025
  nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
1761
2026
  const directCards = this.buildDirectTargetCards(
1762
2027
  runtime,
@@ -1768,15 +2033,30 @@ var init_prescribed = __esm({
1768
2033
  courseId,
1769
2034
  emittedIds
1770
2035
  );
1771
- emitted.push(...directCards, ...supportCards);
2036
+ const discoveredSupportCards = this.buildDiscoveredSupportCards(
2037
+ runtime,
2038
+ courseId,
2039
+ emittedIds
2040
+ );
2041
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
1772
2042
  }
1773
2043
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1774
2044
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
1775
2045
  boostTags: hintSummary.boostTags,
1776
2046
  _label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
1777
2047
  } : void 0;
2048
+ if (hints) {
2049
+ const tagEntries = Object.entries(hints.boostTags ?? {});
2050
+ logger.info(
2051
+ `[Prescribed] Emitting ${tagEntries.length} boost hint(s): ` + tagEntries.map(([tag, mult]) => `${tag}\xD7${mult.toFixed(1)}`).join(", ")
2052
+ );
2053
+ } else {
2054
+ logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
2055
+ }
1778
2056
  if (emitted.length === 0) {
1779
- logger.debug("[Prescribed] No prescribed targets/support emitted this run");
2057
+ logger.info(
2058
+ "[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
2059
+ );
1780
2060
  await this.putStrategyState(nextState).catch((e) => {
1781
2061
  logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
1782
2062
  });
@@ -1809,7 +2089,7 @@ var init_prescribed = __esm({
1809
2089
  logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
1810
2090
  });
1811
2091
  logger.info(
1812
- `[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)`
2092
+ `[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, ${finalCards.filter((c) => c.provenance[0]?.reason.includes("mode=discovered-support")).length} discovered support)`
1813
2093
  );
1814
2094
  return hints ? { cards: finalCards, hints } : { cards: finalCards };
1815
2095
  }
@@ -1839,9 +2119,15 @@ var init_prescribed = __esm({
1839
2119
  const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
1840
2120
  const groups = groupsRaw.map((raw, i) => ({
1841
2121
  id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
1842
- targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
1843
- supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
1844
- supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
2122
+ targetCardIds: dedupe(
2123
+ Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []
2124
+ ),
2125
+ supportCardIds: dedupe(
2126
+ Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []
2127
+ ),
2128
+ supportTagPatterns: dedupe(
2129
+ Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []
2130
+ ),
1845
2131
  freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
1846
2132
  maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
1847
2133
  maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
@@ -1858,7 +2144,7 @@ var init_prescribed = __esm({
1858
2144
  }
1859
2145
  async loadHierarchyConfigs() {
1860
2146
  try {
1861
- const strategies = await this.course.getNavigationStrategies();
2147
+ const strategies = await this.course.getAllNavigationStrategies();
1862
2148
  return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
1863
2149
  try {
1864
2150
  const parsed = JSON.parse(s.serializedData);
@@ -1881,6 +2167,7 @@ var init_prescribed = __esm({
1881
2167
  activeIds,
1882
2168
  seenIds,
1883
2169
  tagsByCard,
2170
+ cardsByTag,
1884
2171
  hierarchyConfigs,
1885
2172
  userTagElo,
1886
2173
  userGlobalElo
@@ -1944,6 +2231,22 @@ var init_prescribed = __esm({
1944
2231
  [...supportTags]
1945
2232
  )
1946
2233
  ]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
2234
+ const discoveredSupportCandidates = blockedTargets.length > 0 && supportTags.size > 0 && supportCandidates.length === 0 ? this.findDiscoveredSupportCards({
2235
+ supportTags: [...supportTags],
2236
+ cardsByTag,
2237
+ activeIds,
2238
+ seenIds,
2239
+ excludedIds: /* @__PURE__ */ new Set([
2240
+ ...group.targetCardIds,
2241
+ ...group.supportCardIds ?? []
2242
+ ]),
2243
+ limit: group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN
2244
+ }) : [];
2245
+ if (blockedTargets.length > 0 && supportTags.size > 0 && discoveredSupportCandidates.length === 0) {
2246
+ logger.info(
2247
+ `[Prescribed] Group '${group.id}' discovered 0 broader support candidates (blocked=${blockedTargets.length}; authoredSupport=${supportCandidates.length})`
2248
+ );
2249
+ }
1947
2250
  const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
1948
2251
  const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
1949
2252
  const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
@@ -1957,6 +2260,7 @@ var init_prescribed = __esm({
1957
2260
  surfaceableTargets,
1958
2261
  targetTags,
1959
2262
  supportCandidates,
2263
+ discoveredSupportCandidates,
1960
2264
  supportTags: [...supportTags],
1961
2265
  pressureMultiplier,
1962
2266
  supportMultiplier,
@@ -2027,6 +2331,33 @@ var init_prescribed = __esm({
2027
2331
  }
2028
2332
  return cards;
2029
2333
  }
2334
+ buildDiscoveredSupportCards(runtime, courseId, emittedIds) {
2335
+ if (runtime.blockedTargets.length === 0 || runtime.discoveredSupportCandidates.length === 0) {
2336
+ return [];
2337
+ }
2338
+ const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
2339
+ const supportIds = runtime.discoveredSupportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
2340
+ const cards = [];
2341
+ for (const cardId of supportIds) {
2342
+ emittedIds.add(cardId);
2343
+ cards.push({
2344
+ cardId,
2345
+ courseId,
2346
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
2347
+ provenance: [
2348
+ {
2349
+ strategy: "prescribed",
2350
+ strategyName: this.strategyName || this.name,
2351
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2352
+ action: "generated",
2353
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
2354
+ reason: `mode=discovered-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}`
2355
+ }
2356
+ ]
2357
+ });
2358
+ }
2359
+ return cards;
2360
+ }
2030
2361
  findSupportCardsByTags(group, tagsByCard, supportTags) {
2031
2362
  if (supportTags.length === 0) {
2032
2363
  return [];
@@ -2049,6 +2380,40 @@ var init_prescribed = __esm({
2049
2380
  }
2050
2381
  return [...candidates];
2051
2382
  }
2383
+ findDiscoveredSupportCards(args) {
2384
+ const { supportTags, cardsByTag, activeIds, seenIds, excludedIds, limit } = args;
2385
+ const byCardId = /* @__PURE__ */ new Map();
2386
+ for (const supportTag of supportTags) {
2387
+ const taggedCards = cardsByTag.get(supportTag) ?? [];
2388
+ for (const cardId of taggedCards) {
2389
+ if (activeIds.has(cardId) || seenIds.has(cardId) || excludedIds.has(cardId)) {
2390
+ continue;
2391
+ }
2392
+ const existing = byCardId.get(cardId);
2393
+ if (existing) {
2394
+ existing.matches += 1;
2395
+ } else {
2396
+ byCardId.set(cardId, { cardId, matches: 1 });
2397
+ }
2398
+ }
2399
+ }
2400
+ const candidates = [...byCardId.values()].sort((a, b) => b.matches - a.matches || a.cardId.localeCompare(b.cardId));
2401
+ const usedStems = /* @__PURE__ */ new Set();
2402
+ const diverse = [];
2403
+ const deferred = [];
2404
+ for (const entry of candidates) {
2405
+ const stem = extractWordStem(entry.cardId);
2406
+ if (!usedStems.has(stem)) {
2407
+ usedStems.add(stem);
2408
+ diverse.push(entry);
2409
+ } else {
2410
+ deferred.push(entry);
2411
+ }
2412
+ }
2413
+ shuffleInPlace(diverse);
2414
+ shuffleInPlace(deferred);
2415
+ return [...diverse, ...deferred].slice(0, limit).map((entry) => entry.cardId);
2416
+ }
2052
2417
  resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
2053
2418
  const supportTags = /* @__PURE__ */ new Set();
2054
2419
  let blocked = false;
@@ -2094,7 +2459,6 @@ var init_prescribed = __esm({
2094
2459
  }
2095
2460
  collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
2096
2461
  if (depth < 0 || visited.has(tag)) return;
2097
- if (this.isHardGatedTag(tag)) return;
2098
2462
  visited.add(tag);
2099
2463
  let walkedFurther = false;
2100
2464
  for (const hierarchy of hierarchyConfigs) {
@@ -2122,9 +2486,6 @@ var init_prescribed = __esm({
2122
2486
  out.add(tag);
2123
2487
  }
2124
2488
  }
2125
- isHardGatedTag(tag) {
2126
- return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
2127
- }
2128
2489
  isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
2129
2490
  if (!userTagElo) return false;
2130
2491
  const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
@@ -3916,6 +4277,32 @@ var init_Pipeline = __esm({
3916
4277
  cards = await this.hydrateTags(cards);
3917
4278
  const tHydrate = performance.now();
3918
4279
  const allCardsBeforeFiltering = [...cards];
4280
+ const pendingHints = this._ephemeralHints;
4281
+ if (pendingHints?.requireCards?.length) {
4282
+ const poolIds = new Set(allCardsBeforeFiltering.map((c) => c.cardId));
4283
+ const missingIds = pendingHints.requireCards.filter(
4284
+ (p) => !p.includes("*") && !poolIds.has(p)
4285
+ );
4286
+ if (missingIds.length > 0) {
4287
+ const fetchedTags = await this.course.getAppliedTagsBatch(missingIds);
4288
+ const courseId = this.course.getCourseID();
4289
+ for (const cardId of missingIds) {
4290
+ allCardsBeforeFiltering.push({
4291
+ cardId,
4292
+ courseId,
4293
+ score: 1,
4294
+ tags: fetchedTags.get(cardId) ?? [],
4295
+ provenance: []
4296
+ });
4297
+ }
4298
+ logger.info(
4299
+ `[Pipeline] Pre-fetched ${missingIds.length} required card(s) into pool: ${missingIds.join(", ")}`
4300
+ );
4301
+ }
4302
+ }
4303
+ const prescribedIds = new Set(
4304
+ cards.filter((c) => c.provenance.some((p) => p.strategy === "prescribed")).map((c) => c.cardId)
4305
+ );
3919
4306
  const filterImpacts = [];
3920
4307
  for (const filter of this.filters) {
3921
4308
  const beforeCount = cards.length;
@@ -3930,6 +4317,17 @@ var init_Pipeline = __esm({
3930
4317
  else passed++;
3931
4318
  }
3932
4319
  filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
4320
+ if (prescribedIds.size > 0) {
4321
+ const survivingIds = new Set(cards.map((c) => c.cardId));
4322
+ const killedPrescribed = [...prescribedIds].filter((id) => !survivingIds.has(id));
4323
+ const zeroedPrescribed = cards.filter((c) => prescribedIds.has(c.cardId) && c.score === 0).map((c) => c.cardId);
4324
+ if (killedPrescribed.length > 0 || zeroedPrescribed.length > 0) {
4325
+ logger.info(
4326
+ `[Pipeline] Filter '${filter.name}' impact on prescribed cards: ` + (killedPrescribed.length > 0 ? `removed=[${killedPrescribed.join(", ")}] ` : "") + (zeroedPrescribed.length > 0 ? `zeroed=[${zeroedPrescribed.join(", ")}]` : "")
4327
+ );
4328
+ killedPrescribed.forEach((id) => prescribedIds.delete(id));
4329
+ }
4330
+ }
3933
4331
  logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
3934
4332
  }
3935
4333
  cards = cards.filter((c) => c.score > 0);
@@ -3966,7 +4364,8 @@ var init_Pipeline = __esm({
3966
4364
  filterImpacts,
3967
4365
  cards,
3968
4366
  result,
3969
- context.userElo
4367
+ context.userElo,
4368
+ hints ?? void 0
3970
4369
  );
3971
4370
  captureRun(report);
3972
4371
  } catch (e) {
@@ -4076,13 +4475,27 @@ var init_Pipeline = __esm({
4076
4475
  }
4077
4476
  }
4078
4477
  const cardIds = new Set(cards.map((c) => c.cardId));
4478
+ const cardMap = new Map(cards.map((c) => [c.cardId, c]));
4079
4479
  const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
4080
- const inject = (card, reason) => {
4081
- if (!cardIds.has(card.cardId)) {
4082
- const floorScore = Math.max(card.score, 1);
4480
+ const applyRequirement = (card, reason) => {
4481
+ const mandatoryScore = Number.POSITIVE_INFINITY;
4482
+ const existing = cardMap.get(card.cardId);
4483
+ if (existing) {
4484
+ if (existing.score < mandatoryScore) {
4485
+ existing.score = mandatoryScore;
4486
+ existing.provenance.push({
4487
+ strategy: "ephemeralHint",
4488
+ strategyId: "ephemeral-hint",
4489
+ strategyName: hintLabel,
4490
+ action: "boosted",
4491
+ score: mandatoryScore,
4492
+ reason: `${reason} (upgrade to mandatory score)`
4493
+ });
4494
+ }
4495
+ } else {
4083
4496
  cards.push({
4084
4497
  ...card,
4085
- score: floorScore,
4498
+ score: mandatoryScore,
4086
4499
  provenance: [
4087
4500
  ...card.provenance,
4088
4501
  {
@@ -4090,25 +4503,41 @@ var init_Pipeline = __esm({
4090
4503
  strategyId: "ephemeral-hint",
4091
4504
  strategyName: hintLabel,
4092
4505
  action: "boosted",
4093
- score: floorScore,
4506
+ score: mandatoryScore,
4094
4507
  reason
4095
4508
  }
4096
4509
  ]
4097
4510
  });
4098
4511
  cardIds.add(card.cardId);
4512
+ cardMap.set(card.cardId, cards[cards.length - 1]);
4099
4513
  }
4100
4514
  };
4101
4515
  if (hints.requireCards?.length) {
4102
4516
  for (const pattern of hints.requireCards) {
4517
+ for (const cardId of cardIds) {
4518
+ if (globMatch(cardId, pattern)) {
4519
+ applyRequirement(cardMap.get(cardId), `requireCard ${pattern}`);
4520
+ }
4521
+ }
4103
4522
  for (const card of allCards) {
4104
- if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
4523
+ if (globMatch(card.cardId, pattern)) {
4524
+ applyRequirement(card, `requireCard ${pattern}`);
4525
+ }
4105
4526
  }
4106
4527
  }
4107
4528
  }
4108
4529
  if (hints.requireTags?.length) {
4109
4530
  for (const pattern of hints.requireTags) {
4531
+ for (const cardId of cardIds) {
4532
+ const card = cardMap.get(cardId);
4533
+ if (cardMatchesTagPattern(card, pattern)) {
4534
+ applyRequirement(card, `requireTag ${pattern}`);
4535
+ }
4536
+ }
4110
4537
  for (const card of allCards) {
4111
- if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
4538
+ if (cardMatchesTagPattern(card, pattern)) {
4539
+ applyRequirement(card, `requireTag ${pattern}`);
4540
+ }
4112
4541
  }
4113
4542
  }
4114
4543
  }
@@ -5262,7 +5691,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
5262
5691
  }
5263
5692
  }
5264
5693
  async getCourseDoc(id, options) {
5265
- return await this.db.get(id, options);
5694
+ return await this.db.get(id, options ?? {});
5266
5695
  }
5267
5696
  async getCourseDocs(ids, options = {}) {
5268
5697
  return await this.db.allDocs({
@@ -5346,7 +5775,9 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
5346
5775
  );
5347
5776
  return pipeline;
5348
5777
  } catch (e) {
5349
- logger.error(`[courseDB] Error creating navigator: ${e}`);
5778
+ const msg = e instanceof Error ? `${e.message}
5779
+ ${e.stack}` : JSON.stringify(e);
5780
+ logger.error(`[courseDB] Error creating navigator: ${msg}`);
5350
5781
  throw e;
5351
5782
  }
5352
5783
  }
@@ -10519,7 +10950,10 @@ var CardHydrationService = class {
10519
10950
  this.hydrationInFlight.add(item.cardID);
10520
10951
  try {
10521
10952
  const courseDB = this.getCourseDB(item.courseID);
10522
- const cardData = await courseDB.getCourseDoc(item.cardID);
10953
+ const [cardData, tagsByCard] = await Promise.all([
10954
+ courseDB.getCourseDoc(item.cardID),
10955
+ courseDB.getAppliedTagsBatch([item.cardID])
10956
+ ]);
10523
10957
  if (!(0, import_common25.isCourseElo)(cardData.elo)) {
10524
10958
  cardData.elo = (0, import_common25.toCourseElo)(cardData.elo);
10525
10959
  }
@@ -10549,7 +10983,8 @@ var CardHydrationService = class {
10549
10983
  this.hydratedCards.set(item.cardID, {
10550
10984
  item,
10551
10985
  view,
10552
- data
10986
+ data,
10987
+ tags: tagsByCard.get(item.cardID) ?? []
10553
10988
  });
10554
10989
  logger.debug(`[CardHydrationService] Hydrated card ${item.cardID}`);
10555
10990
  } finally {