@vue-skuilder/db 0.1.32-e → 0.1.33-vite8-4

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.mjs CHANGED
@@ -865,8 +865,9 @@ function getOrigin(card) {
865
865
  const firstEntry = card.provenance[0];
866
866
  if (!firstEntry) return "unknown";
867
867
  const reason = firstEntry.reason?.toLowerCase() || "";
868
- if (reason.includes("new card")) return "new";
869
- if (reason.includes("review")) return "review";
868
+ const strategy = firstEntry.strategy?.toLowerCase() || "";
869
+ if (reason.includes("new card") || strategy.includes("elo")) return "new";
870
+ if (reason.includes("review") || strategy.includes("srs")) return "review";
870
871
  return "unknown";
871
872
  }
872
873
  function captureRun(report) {
@@ -886,12 +887,13 @@ function parseCardElo(provenance) {
886
887
  const match = eloEntry.reason.match(/card:\s*(\d+)/);
887
888
  return match ? parseInt(match[1], 10) : void 0;
888
889
  }
889
- function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
890
+ function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
890
891
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
891
892
  const cards = allCards.map((card) => ({
892
893
  cardId: card.cardId,
893
894
  courseId: card.courseId,
894
895
  origin: getOrigin(card),
896
+ generator: card.provenance[0]?.strategyName || card.provenance[0]?.strategy,
895
897
  finalScore: card.score,
896
898
  cardElo: parseCardElo(card.provenance),
897
899
  provenance: card.provenance,
@@ -908,6 +910,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
908
910
  generators,
909
911
  generatedCount,
910
912
  filters,
913
+ hints,
911
914
  finalCount: selectedCards.length,
912
915
  reviewsSelected,
913
916
  newSelected,
@@ -947,13 +950,169 @@ function printRunSummary(run) {
947
950
  );
948
951
  console.groupEnd();
949
952
  }
953
+ function renderUI() {
954
+ if (!_uiContainer) return;
955
+ const runs = runHistory;
956
+ const selectedRun = _selectedRunIndex !== null ? runs[_selectedRunIndex] : null;
957
+ const styles = `
958
+ #sk-pipeline-debugger {
959
+ position: fixed;
960
+ top: 0;
961
+ left: 0;
962
+ width: 100vw;
963
+ height: 100vh;
964
+ background: #f8f9fa;
965
+ color: #212529;
966
+ z-index: 999999;
967
+ display: flex;
968
+ flex-direction: column;
969
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
970
+ font-size: 14px;
971
+ }
972
+ #sk-pipeline-debugger header {
973
+ padding: 1rem;
974
+ background: #343a40;
975
+ color: white;
976
+ display: flex;
977
+ justify-content: space-between;
978
+ align-items: center;
979
+ }
980
+ #sk-pipeline-debugger .container {
981
+ display: flex;
982
+ flex: 1;
983
+ overflow: hidden;
984
+ }
985
+ #sk-pipeline-debugger .sidebar {
986
+ width: 300px;
987
+ border-right: 1px solid #dee2e6;
988
+ overflow-y: auto;
989
+ background: white;
990
+ }
991
+ #sk-pipeline-debugger .main-content {
992
+ flex: 1;
993
+ overflow-y: auto;
994
+ padding: 1.5rem;
995
+ }
996
+ #sk-pipeline-debugger .run-item {
997
+ padding: 0.75rem 1rem;
998
+ border-bottom: 1px solid #eee;
999
+ cursor: pointer;
1000
+ }
1001
+ #sk-pipeline-debugger .run-item:hover { background: #f1f3f5; }
1002
+ #sk-pipeline-debugger .run-item.active { background: #e9ecef; border-left: 4px solid #007bff; }
1003
+ #sk-pipeline-debugger h2, #sk-pipeline-debugger h3 { margin-top: 0; }
1004
+ #sk-pipeline-debugger table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; background: white; }
1005
+ #sk-pipeline-debugger th, #sk-pipeline-debugger td { border: 1px solid #dee2e6; padding: 0.5rem; text-align: left; }
1006
+ #sk-pipeline-debugger th { background: #f1f3f5; }
1007
+ #sk-pipeline-debugger code { background: #f1f3f5; padding: 0.1rem 0.3rem; border-radius: 3px; font-family: monospace; }
1008
+ #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
1009
+ #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
1010
+ #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; }
1011
+ `;
1012
+ const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
1013
+ (r, i) => `
1014
+ <div class="run-item ${i === _selectedRunIndex ? "active" : ""}" onclick="window.skuilder.pipeline._selectRun(${i})">
1015
+ <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
1016
+ <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
1017
+ <small>${r.finalCount} cards selected</small>
1018
+ </div>
1019
+ `
1020
+ ).join("");
1021
+ let detailsHtml = '<div style="color: #6c757d; text-align: center; margin-top: 5rem;">Select a run to see details</div>';
1022
+ if (selectedRun) {
1023
+ const filteredCards = selectedRun.cards.filter(
1024
+ (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
1025
+ );
1026
+ detailsHtml = `
1027
+ <h2>Run: ${selectedRun.runId}</h2>
1028
+ <p>
1029
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
1030
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
1031
+ <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
1032
+ </p>
1033
+
1034
+ <h3>Pipeline Config</h3>
1035
+ <table>
1036
+ <tr><th>Generator</th><td>${selectedRun.generatorName} (${selectedRun.generatedCount} candidates)</td></tr>
1037
+ ${(selectedRun.generators || []).map(
1038
+ (g) => `
1039
+ <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>
1040
+ `
1041
+ ).join("")}
1042
+ </table>
1043
+
1044
+ ${selectedRun.hints ? `
1045
+ <h3>Ephemeral Hints</h3>
1046
+ <table>
1047
+ ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
1048
+ ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
1049
+ ${selectedRun.hints.boostCards ? `<tr><th>Boost Cards</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostCards, null, 2)}</pre></td></tr>` : ""}
1050
+ ${selectedRun.hints.requireTags ? `<tr><th>Require Tags</th><td>${selectedRun.hints.requireTags.join(", ")}</td></tr>` : ""}
1051
+ ${selectedRun.hints.requireCards ? `<tr><th>Require Cards</th><td>${selectedRun.hints.requireCards.join(", ")}</td></tr>` : ""}
1052
+ ${selectedRun.hints.excludeTags ? `<tr><th>Exclude Tags</th><td>${selectedRun.hints.excludeTags.join(", ")}</td></tr>` : ""}
1053
+ ${selectedRun.hints.excludeCards ? `<tr><th>Exclude Cards</th><td>${selectedRun.hints.excludeCards.join(", ")}</td></tr>` : ""}
1054
+ </table>
1055
+ ` : ""}
1056
+
1057
+ <h3>Filter Impact</h3>
1058
+ <table>
1059
+ <thead><tr><th>Filter</th><th>Boosted</th><th>Penalized</th><th>Passed</th><th>Removed</th></tr></thead>
1060
+ <tbody>
1061
+ ${selectedRun.filters.map(
1062
+ (f) => `
1063
+ <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>
1064
+ `
1065
+ ).join("")}
1066
+ </tbody>
1067
+ </table>
1068
+
1069
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
1070
+ <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
1071
+
1072
+ <table>
1073
+ <thead><tr><th>ID</th><th>Generator</th><th>Origin</th><th>Score</th><th>Selected</th></tr></thead>
1074
+ <tbody>
1075
+ ${filteredCards.map(
1076
+ (c) => `
1077
+ <tr>
1078
+ <td><code>${c.cardId}</code></td>
1079
+ <td>${c.generator || "unknown"}</td>
1080
+ <td>${c.origin}</td>
1081
+ <td>${c.finalScore.toFixed(3)}</td>
1082
+ <td>${c.selected ? "\u2705" : "\u274C"}</td>
1083
+ </tr>
1084
+ ${c.selected || _cardSearchQuery ? `
1085
+ <tr>
1086
+ <td colspan="5">
1087
+ <div class="provenance">${formatProvenance(c.provenance)}</div>
1088
+ </td>
1089
+ </tr>
1090
+ ` : ""}
1091
+ `
1092
+ ).join("")}
1093
+ </tbody>
1094
+ </table>
1095
+ `;
1096
+ }
1097
+ _uiContainer.innerHTML = `
1098
+ <style>${styles}</style>
1099
+ <header>
1100
+ <strong>Pipeline Debugger</strong>
1101
+ <button class="close-btn" onclick="window.skuilder.pipeline.ui()">Close</button>
1102
+ </header>
1103
+ <div class="container">
1104
+ <div class="sidebar">${runListHtml}</div>
1105
+ <div class="main-content">${detailsHtml}</div>
1106
+ </div>
1107
+ `;
1108
+ }
950
1109
  function mountPipelineDebugger() {
951
1110
  if (typeof window === "undefined") return;
952
1111
  const win = window;
953
1112
  win.skuilder = win.skuilder || {};
954
1113
  win.skuilder.pipeline = pipelineDebugAPI;
955
1114
  }
956
- var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
1115
+ var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
957
1116
  var init_PipelineDebugger = __esm({
958
1117
  "src/core/navigators/PipelineDebugger.ts"() {
959
1118
  "use strict";
@@ -962,6 +1121,9 @@ var init_PipelineDebugger = __esm({
962
1121
  _activePipeline = null;
963
1122
  MAX_RUNS = 10;
964
1123
  runHistory = [];
1124
+ _uiContainer = null;
1125
+ _selectedRunIndex = null;
1126
+ _cardSearchQuery = "";
965
1127
  pipelineDebugAPI = {
966
1128
  /**
967
1129
  * Get raw run history for programmatic access.
@@ -1101,16 +1263,20 @@ var init_PipelineDebugger = __esm({
1101
1263
  const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
1102
1264
  const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
1103
1265
  const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
1266
+ const supportCard = reason.match(/supportCard=([^;]+)/)?.[1] ?? "none";
1104
1267
  const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
1105
1268
  const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
1269
+ const supportSource = mode === "discovered-support" ? "discovered" : mode === "support" ? "authored" : "n/a";
1106
1270
  return {
1107
1271
  group: parsedGroup,
1108
1272
  mode,
1273
+ supportSource,
1109
1274
  cardId: card.cardId,
1110
1275
  selected: card.selected ? "yes" : "no",
1111
1276
  finalScore: card.finalScore.toFixed(3),
1112
1277
  blocked,
1113
1278
  blockedTargets,
1279
+ supportCard,
1114
1280
  supportTags,
1115
1281
  multiplier
1116
1282
  };
@@ -1126,6 +1292,8 @@ var init_PipelineDebugger = __esm({
1126
1292
  const selectedRows = rows.filter((r) => r.selected === "yes");
1127
1293
  const blockedTargetSet = /* @__PURE__ */ new Set();
1128
1294
  const supportTagSet = /* @__PURE__ */ new Set();
1295
+ const authoredSupportSet = /* @__PURE__ */ new Set();
1296
+ const discoveredSupportSet = /* @__PURE__ */ new Set();
1129
1297
  for (const row of rows) {
1130
1298
  if (row.blockedTargets && row.blockedTargets !== "none") {
1131
1299
  row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
@@ -1133,6 +1301,13 @@ var init_PipelineDebugger = __esm({
1133
1301
  if (row.supportTags && row.supportTags !== "none") {
1134
1302
  row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
1135
1303
  }
1304
+ if (row.supportCard && row.supportCard !== "none") {
1305
+ if (row.supportSource === "discovered") {
1306
+ discoveredSupportSet.add(row.supportCard);
1307
+ } else if (row.supportSource === "authored") {
1308
+ authoredSupportSet.add(row.supportCard);
1309
+ }
1310
+ }
1136
1311
  }
1137
1312
  logger.info(`Prescribed cards in run: ${rows.length}`);
1138
1313
  logger.info(`Selected prescribed cards: ${selectedRows.length}`);
@@ -1142,6 +1317,12 @@ var init_PipelineDebugger = __esm({
1142
1317
  logger.info(
1143
1318
  `Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
1144
1319
  );
1320
+ logger.info(
1321
+ `Authored support cards emitted: ${authoredSupportSet.size > 0 ? [...authoredSupportSet].join(", ") : "none"}`
1322
+ );
1323
+ logger.info(
1324
+ `Discovered support cards emitted: ${discoveredSupportSet.size > 0 ? [...discoveredSupportSet].join(", ") : "none"}`
1325
+ );
1145
1326
  console.groupEnd();
1146
1327
  },
1147
1328
  /**
@@ -1276,6 +1457,39 @@ var init_PipelineDebugger = __esm({
1276
1457
  Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
1277
1458
  );
1278
1459
  },
1460
+ /**
1461
+ * Toggle the full-screen UI debugger.
1462
+ */
1463
+ ui() {
1464
+ if (_uiContainer) {
1465
+ document.body.removeChild(_uiContainer);
1466
+ _uiContainer = null;
1467
+ return;
1468
+ }
1469
+ _uiContainer = document.createElement("div");
1470
+ _uiContainer.id = "sk-pipeline-debugger";
1471
+ document.body.appendChild(_uiContainer);
1472
+ if (_selectedRunIndex === null && runHistory.length > 0) {
1473
+ _selectedRunIndex = 0;
1474
+ }
1475
+ renderUI();
1476
+ },
1477
+ /**
1478
+ * Internal UI helpers
1479
+ * @internal
1480
+ */
1481
+ _selectRun(index) {
1482
+ _selectedRunIndex = index;
1483
+ renderUI();
1484
+ },
1485
+ /**
1486
+ * Internal UI helpers
1487
+ * @internal
1488
+ */
1489
+ _setSearch(query) {
1490
+ _cardSearchQuery = query;
1491
+ renderUI();
1492
+ },
1279
1493
  /**
1280
1494
  * Show help.
1281
1495
  */
@@ -1284,6 +1498,7 @@ var init_PipelineDebugger = __esm({
1284
1498
  \u{1F527} Pipeline Debug API
1285
1499
 
1286
1500
  Commands:
1501
+ .ui() Toggle full-screen UI debugger
1287
1502
  .showLastRun() Show summary of most recent pipeline run
1288
1503
  .showRun(id|index) Show summary of a specific run (by index or ID suffix)
1289
1504
  .showCard(cardId) Show provenance trail for a specific card
@@ -1300,6 +1515,7 @@ Commands:
1300
1515
  .help() Show this help message
1301
1516
 
1302
1517
  Example:
1518
+ window.skuilder.pipeline.ui()
1303
1519
  window.skuilder.pipeline.showLastRun()
1304
1520
  window.skuilder.pipeline.showRun(1)
1305
1521
  await window.skuilder.pipeline.diagnoseCardSpace()
@@ -1462,7 +1678,7 @@ var init_CompositeGenerator = __esm({
1462
1678
  for (const [, items] of byCardId) {
1463
1679
  const cards2 = items.map((i) => i.card);
1464
1680
  const aggregatedScore = this.aggregateScores(items);
1465
- const finalScore = Math.min(1, aggregatedScore);
1681
+ const finalScore = Math.max(0, aggregatedScore);
1466
1682
  const mergedProvenance = cards2.flatMap((c) => c.provenance);
1467
1683
  const initialScore = cards2[0].score;
1468
1684
  const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
@@ -1659,10 +1875,26 @@ function matchesTagPattern(tag, pattern) {
1659
1875
  const re = new RegExp(`^${escaped}$`);
1660
1876
  return re.test(tag);
1661
1877
  }
1878
+ function extractWordStem(cardId) {
1879
+ for (const prefix of ["c-ml-", "c-ws-", "c-spelling-"]) {
1880
+ if (cardId.startsWith(prefix)) {
1881
+ const rest = cardId.slice(prefix.length);
1882
+ const lastDash = rest.lastIndexOf("-");
1883
+ return lastDash > 0 ? rest.slice(0, lastDash) : rest;
1884
+ }
1885
+ }
1886
+ return cardId;
1887
+ }
1888
+ function shuffleInPlace(arr) {
1889
+ for (let i = arr.length - 1; i > 0; i--) {
1890
+ const j = Math.floor(Math.random() * (i + 1));
1891
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1892
+ }
1893
+ }
1662
1894
  function pickTopByScore(cards, limit) {
1663
1895
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1664
1896
  }
1665
- 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;
1897
+ 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;
1666
1898
  var init_prescribed = __esm({
1667
1899
  "src/core/navigators/generators/prescribed.ts"() {
1668
1900
  "use strict";
@@ -1675,11 +1907,10 @@ var init_prescribed = __esm({
1675
1907
  DEFAULT_MIN_COUNT = 3;
1676
1908
  BASE_TARGET_SCORE = 1;
1677
1909
  BASE_SUPPORT_SCORE = 0.8;
1910
+ DISCOVERED_SUPPORT_SCORE = 12;
1678
1911
  MAX_TARGET_MULTIPLIER = 8;
1679
1912
  MAX_SUPPORT_MULTIPLIER = 4;
1680
- LOCKED_TAG_PREFIXES = ["concept:"];
1681
- LESSON_GATE_PENALTY_TAG_HINT = "concept:";
1682
- PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
1913
+ PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
1683
1914
  PrescribedCardsGenerator = class extends ContentNavigator {
1684
1915
  name;
1685
1916
  config;
@@ -1715,6 +1946,20 @@ var init_prescribed = __esm({
1715
1946
  const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
1716
1947
  const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
1717
1948
  const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
1949
+ const courseTagDocs = await this.course.getCourseTagStubs().catch(
1950
+ () => ({
1951
+ rows: [],
1952
+ offset: 0,
1953
+ total_rows: 0
1954
+ })
1955
+ );
1956
+ const cardsByTag = /* @__PURE__ */ new Map();
1957
+ for (const row of courseTagDocs.rows ?? []) {
1958
+ const tagDoc = row.doc;
1959
+ if (tagDoc?.name && Array.isArray(tagDoc.taggedCards)) {
1960
+ cardsByTag.set(tagDoc.name, [...tagDoc.taggedCards]);
1961
+ }
1962
+ }
1718
1963
  const nextState = {
1719
1964
  updatedAt: isoNow(),
1720
1965
  groups: {}
@@ -1729,11 +1974,31 @@ var init_prescribed = __esm({
1729
1974
  activeIds,
1730
1975
  seenIds,
1731
1976
  tagsByCard,
1977
+ cardsByTag,
1732
1978
  hierarchyConfigs,
1733
1979
  userTagElo,
1734
1980
  userGlobalElo
1735
1981
  });
1736
1982
  groupRuntimes.push(runtime);
1983
+ logger.info(
1984
+ `[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)}`
1985
+ );
1986
+ if (runtime.blockedTargets.length > 0) {
1987
+ logger.info(
1988
+ `[Prescribed] Group '${group.id}' blocked targets: ${runtime.blockedTargets.join(", ")}`
1989
+ );
1990
+ logger.info(
1991
+ `[Prescribed] Group '${group.id}' support tags needed: ${runtime.supportTags.join(", ") || "(none)"}`
1992
+ );
1993
+ logger.info(
1994
+ `[Prescribed] Group '${group.id}' escalation mode: ` + (runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support-candidates" : "boost-only")
1995
+ );
1996
+ if (runtime.discoveredSupportCandidates.length > 0) {
1997
+ logger.info(
1998
+ `[Prescribed] Group '${group.id}' discovered support candidates: ${runtime.discoveredSupportCandidates.join(", ")}`
1999
+ );
2000
+ }
2001
+ }
1737
2002
  nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
1738
2003
  const directCards = this.buildDirectTargetCards(
1739
2004
  runtime,
@@ -1745,15 +2010,30 @@ var init_prescribed = __esm({
1745
2010
  courseId,
1746
2011
  emittedIds
1747
2012
  );
1748
- emitted.push(...directCards, ...supportCards);
2013
+ const discoveredSupportCards = this.buildDiscoveredSupportCards(
2014
+ runtime,
2015
+ courseId,
2016
+ emittedIds
2017
+ );
2018
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
1749
2019
  }
1750
2020
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1751
2021
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
1752
2022
  boostTags: hintSummary.boostTags,
1753
2023
  _label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
1754
2024
  } : void 0;
2025
+ if (hints) {
2026
+ const tagEntries = Object.entries(hints.boostTags ?? {});
2027
+ logger.info(
2028
+ `[Prescribed] Emitting ${tagEntries.length} boost hint(s): ` + tagEntries.map(([tag, mult]) => `${tag}\xD7${mult.toFixed(1)}`).join(", ")
2029
+ );
2030
+ } else {
2031
+ logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
2032
+ }
1755
2033
  if (emitted.length === 0) {
1756
- logger.debug("[Prescribed] No prescribed targets/support emitted this run");
2034
+ logger.info(
2035
+ "[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
2036
+ );
1757
2037
  await this.putStrategyState(nextState).catch((e) => {
1758
2038
  logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
1759
2039
  });
@@ -1786,7 +2066,7 @@ var init_prescribed = __esm({
1786
2066
  logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
1787
2067
  });
1788
2068
  logger.info(
1789
- `[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)`
2069
+ `[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)`
1790
2070
  );
1791
2071
  return hints ? { cards: finalCards, hints } : { cards: finalCards };
1792
2072
  }
@@ -1816,9 +2096,15 @@ var init_prescribed = __esm({
1816
2096
  const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
1817
2097
  const groups = groupsRaw.map((raw, i) => ({
1818
2098
  id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
1819
- targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
1820
- supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
1821
- supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
2099
+ targetCardIds: dedupe(
2100
+ Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []
2101
+ ),
2102
+ supportCardIds: dedupe(
2103
+ Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []
2104
+ ),
2105
+ supportTagPatterns: dedupe(
2106
+ Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []
2107
+ ),
1822
2108
  freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
1823
2109
  maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
1824
2110
  maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
@@ -1835,7 +2121,7 @@ var init_prescribed = __esm({
1835
2121
  }
1836
2122
  async loadHierarchyConfigs() {
1837
2123
  try {
1838
- const strategies = await this.course.getNavigationStrategies();
2124
+ const strategies = await this.course.getAllNavigationStrategies();
1839
2125
  return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
1840
2126
  try {
1841
2127
  const parsed = JSON.parse(s.serializedData);
@@ -1858,6 +2144,7 @@ var init_prescribed = __esm({
1858
2144
  activeIds,
1859
2145
  seenIds,
1860
2146
  tagsByCard,
2147
+ cardsByTag,
1861
2148
  hierarchyConfigs,
1862
2149
  userTagElo,
1863
2150
  userGlobalElo
@@ -1921,6 +2208,22 @@ var init_prescribed = __esm({
1921
2208
  [...supportTags]
1922
2209
  )
1923
2210
  ]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
2211
+ const discoveredSupportCandidates = blockedTargets.length > 0 && supportTags.size > 0 && supportCandidates.length === 0 ? this.findDiscoveredSupportCards({
2212
+ supportTags: [...supportTags],
2213
+ cardsByTag,
2214
+ activeIds,
2215
+ seenIds,
2216
+ excludedIds: /* @__PURE__ */ new Set([
2217
+ ...group.targetCardIds,
2218
+ ...group.supportCardIds ?? []
2219
+ ]),
2220
+ limit: group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN
2221
+ }) : [];
2222
+ if (blockedTargets.length > 0 && supportTags.size > 0 && discoveredSupportCandidates.length === 0) {
2223
+ logger.info(
2224
+ `[Prescribed] Group '${group.id}' discovered 0 broader support candidates (blocked=${blockedTargets.length}; authoredSupport=${supportCandidates.length})`
2225
+ );
2226
+ }
1924
2227
  const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
1925
2228
  const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
1926
2229
  const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
@@ -1934,6 +2237,7 @@ var init_prescribed = __esm({
1934
2237
  surfaceableTargets,
1935
2238
  targetTags,
1936
2239
  supportCandidates,
2240
+ discoveredSupportCandidates,
1937
2241
  supportTags: [...supportTags],
1938
2242
  pressureMultiplier,
1939
2243
  supportMultiplier,
@@ -2004,6 +2308,33 @@ var init_prescribed = __esm({
2004
2308
  }
2005
2309
  return cards;
2006
2310
  }
2311
+ buildDiscoveredSupportCards(runtime, courseId, emittedIds) {
2312
+ if (runtime.blockedTargets.length === 0 || runtime.discoveredSupportCandidates.length === 0) {
2313
+ return [];
2314
+ }
2315
+ const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
2316
+ const supportIds = runtime.discoveredSupportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
2317
+ const cards = [];
2318
+ for (const cardId of supportIds) {
2319
+ emittedIds.add(cardId);
2320
+ cards.push({
2321
+ cardId,
2322
+ courseId,
2323
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
2324
+ provenance: [
2325
+ {
2326
+ strategy: "prescribed",
2327
+ strategyName: this.strategyName || this.name,
2328
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2329
+ action: "generated",
2330
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
2331
+ 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}`
2332
+ }
2333
+ ]
2334
+ });
2335
+ }
2336
+ return cards;
2337
+ }
2007
2338
  findSupportCardsByTags(group, tagsByCard, supportTags) {
2008
2339
  if (supportTags.length === 0) {
2009
2340
  return [];
@@ -2026,6 +2357,40 @@ var init_prescribed = __esm({
2026
2357
  }
2027
2358
  return [...candidates];
2028
2359
  }
2360
+ findDiscoveredSupportCards(args) {
2361
+ const { supportTags, cardsByTag, activeIds, seenIds, excludedIds, limit } = args;
2362
+ const byCardId = /* @__PURE__ */ new Map();
2363
+ for (const supportTag of supportTags) {
2364
+ const taggedCards = cardsByTag.get(supportTag) ?? [];
2365
+ for (const cardId of taggedCards) {
2366
+ if (activeIds.has(cardId) || seenIds.has(cardId) || excludedIds.has(cardId)) {
2367
+ continue;
2368
+ }
2369
+ const existing = byCardId.get(cardId);
2370
+ if (existing) {
2371
+ existing.matches += 1;
2372
+ } else {
2373
+ byCardId.set(cardId, { cardId, matches: 1 });
2374
+ }
2375
+ }
2376
+ }
2377
+ const candidates = [...byCardId.values()].sort((a, b) => b.matches - a.matches || a.cardId.localeCompare(b.cardId));
2378
+ const usedStems = /* @__PURE__ */ new Set();
2379
+ const diverse = [];
2380
+ const deferred = [];
2381
+ for (const entry of candidates) {
2382
+ const stem = extractWordStem(entry.cardId);
2383
+ if (!usedStems.has(stem)) {
2384
+ usedStems.add(stem);
2385
+ diverse.push(entry);
2386
+ } else {
2387
+ deferred.push(entry);
2388
+ }
2389
+ }
2390
+ shuffleInPlace(diverse);
2391
+ shuffleInPlace(deferred);
2392
+ return [...diverse, ...deferred].slice(0, limit).map((entry) => entry.cardId);
2393
+ }
2029
2394
  resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
2030
2395
  const supportTags = /* @__PURE__ */ new Set();
2031
2396
  let blocked = false;
@@ -2071,7 +2436,6 @@ var init_prescribed = __esm({
2071
2436
  }
2072
2437
  collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
2073
2438
  if (depth < 0 || visited.has(tag)) return;
2074
- if (this.isHardGatedTag(tag)) return;
2075
2439
  visited.add(tag);
2076
2440
  let walkedFurther = false;
2077
2441
  for (const hierarchy of hierarchyConfigs) {
@@ -2099,9 +2463,6 @@ var init_prescribed = __esm({
2099
2463
  out.add(tag);
2100
2464
  }
2101
2465
  }
2102
- isHardGatedTag(tag) {
2103
- return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
2104
- }
2105
2466
  isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
2106
2467
  if (!userTagElo) return false;
2107
2468
  const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
@@ -3893,6 +4254,32 @@ var init_Pipeline = __esm({
3893
4254
  cards = await this.hydrateTags(cards);
3894
4255
  const tHydrate = performance.now();
3895
4256
  const allCardsBeforeFiltering = [...cards];
4257
+ const pendingHints = this._ephemeralHints;
4258
+ if (pendingHints?.requireCards?.length) {
4259
+ const poolIds = new Set(allCardsBeforeFiltering.map((c) => c.cardId));
4260
+ const missingIds = pendingHints.requireCards.filter(
4261
+ (p) => !p.includes("*") && !poolIds.has(p)
4262
+ );
4263
+ if (missingIds.length > 0) {
4264
+ const fetchedTags = await this.course.getAppliedTagsBatch(missingIds);
4265
+ const courseId = this.course.getCourseID();
4266
+ for (const cardId of missingIds) {
4267
+ allCardsBeforeFiltering.push({
4268
+ cardId,
4269
+ courseId,
4270
+ score: 1,
4271
+ tags: fetchedTags.get(cardId) ?? [],
4272
+ provenance: []
4273
+ });
4274
+ }
4275
+ logger.info(
4276
+ `[Pipeline] Pre-fetched ${missingIds.length} required card(s) into pool: ${missingIds.join(", ")}`
4277
+ );
4278
+ }
4279
+ }
4280
+ const prescribedIds = new Set(
4281
+ cards.filter((c) => c.provenance.some((p) => p.strategy === "prescribed")).map((c) => c.cardId)
4282
+ );
3896
4283
  const filterImpacts = [];
3897
4284
  for (const filter of this.filters) {
3898
4285
  const beforeCount = cards.length;
@@ -3907,6 +4294,17 @@ var init_Pipeline = __esm({
3907
4294
  else passed++;
3908
4295
  }
3909
4296
  filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
4297
+ if (prescribedIds.size > 0) {
4298
+ const survivingIds = new Set(cards.map((c) => c.cardId));
4299
+ const killedPrescribed = [...prescribedIds].filter((id) => !survivingIds.has(id));
4300
+ const zeroedPrescribed = cards.filter((c) => prescribedIds.has(c.cardId) && c.score === 0).map((c) => c.cardId);
4301
+ if (killedPrescribed.length > 0 || zeroedPrescribed.length > 0) {
4302
+ logger.info(
4303
+ `[Pipeline] Filter '${filter.name}' impact on prescribed cards: ` + (killedPrescribed.length > 0 ? `removed=[${killedPrescribed.join(", ")}] ` : "") + (zeroedPrescribed.length > 0 ? `zeroed=[${zeroedPrescribed.join(", ")}]` : "")
4304
+ );
4305
+ killedPrescribed.forEach((id) => prescribedIds.delete(id));
4306
+ }
4307
+ }
3910
4308
  logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
3911
4309
  }
3912
4310
  cards = cards.filter((c) => c.score > 0);
@@ -3943,7 +4341,8 @@ var init_Pipeline = __esm({
3943
4341
  filterImpacts,
3944
4342
  cards,
3945
4343
  result,
3946
- context.userElo
4344
+ context.userElo,
4345
+ hints ?? void 0
3947
4346
  );
3948
4347
  captureRun(report);
3949
4348
  } catch (e) {
@@ -4053,13 +4452,27 @@ var init_Pipeline = __esm({
4053
4452
  }
4054
4453
  }
4055
4454
  const cardIds = new Set(cards.map((c) => c.cardId));
4455
+ const cardMap = new Map(cards.map((c) => [c.cardId, c]));
4056
4456
  const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
4057
- const inject = (card, reason) => {
4058
- if (!cardIds.has(card.cardId)) {
4059
- const floorScore = Math.max(card.score, 1);
4457
+ const applyRequirement = (card, reason) => {
4458
+ const mandatoryScore = Number.POSITIVE_INFINITY;
4459
+ const existing = cardMap.get(card.cardId);
4460
+ if (existing) {
4461
+ if (existing.score < mandatoryScore) {
4462
+ existing.score = mandatoryScore;
4463
+ existing.provenance.push({
4464
+ strategy: "ephemeralHint",
4465
+ strategyId: "ephemeral-hint",
4466
+ strategyName: hintLabel,
4467
+ action: "boosted",
4468
+ score: mandatoryScore,
4469
+ reason: `${reason} (upgrade to mandatory score)`
4470
+ });
4471
+ }
4472
+ } else {
4060
4473
  cards.push({
4061
4474
  ...card,
4062
- score: floorScore,
4475
+ score: mandatoryScore,
4063
4476
  provenance: [
4064
4477
  ...card.provenance,
4065
4478
  {
@@ -4067,25 +4480,41 @@ var init_Pipeline = __esm({
4067
4480
  strategyId: "ephemeral-hint",
4068
4481
  strategyName: hintLabel,
4069
4482
  action: "boosted",
4070
- score: floorScore,
4483
+ score: mandatoryScore,
4071
4484
  reason
4072
4485
  }
4073
4486
  ]
4074
4487
  });
4075
4488
  cardIds.add(card.cardId);
4489
+ cardMap.set(card.cardId, cards[cards.length - 1]);
4076
4490
  }
4077
4491
  };
4078
4492
  if (hints.requireCards?.length) {
4079
4493
  for (const pattern of hints.requireCards) {
4494
+ for (const cardId of cardIds) {
4495
+ if (globMatch(cardId, pattern)) {
4496
+ applyRequirement(cardMap.get(cardId), `requireCard ${pattern}`);
4497
+ }
4498
+ }
4080
4499
  for (const card of allCards) {
4081
- if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
4500
+ if (globMatch(card.cardId, pattern)) {
4501
+ applyRequirement(card, `requireCard ${pattern}`);
4502
+ }
4082
4503
  }
4083
4504
  }
4084
4505
  }
4085
4506
  if (hints.requireTags?.length) {
4086
4507
  for (const pattern of hints.requireTags) {
4508
+ for (const cardId of cardIds) {
4509
+ const card = cardMap.get(cardId);
4510
+ if (cardMatchesTagPattern(card, pattern)) {
4511
+ applyRequirement(card, `requireTag ${pattern}`);
4512
+ }
4513
+ }
4087
4514
  for (const card of allCards) {
4088
- if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
4515
+ if (cardMatchesTagPattern(card, pattern)) {
4516
+ applyRequirement(card, `requireTag ${pattern}`);
4517
+ }
4089
4518
  }
4090
4519
  }
4091
4520
  }