@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.
@@ -659,8 +659,9 @@ function getOrigin(card) {
659
659
  const firstEntry = card.provenance[0];
660
660
  if (!firstEntry) return "unknown";
661
661
  const reason = firstEntry.reason?.toLowerCase() || "";
662
- if (reason.includes("new card")) return "new";
663
- if (reason.includes("review")) return "review";
662
+ const strategy = firstEntry.strategy?.toLowerCase() || "";
663
+ if (reason.includes("new card") || strategy.includes("elo")) return "new";
664
+ if (reason.includes("review") || strategy.includes("srs")) return "review";
664
665
  return "unknown";
665
666
  }
666
667
  function captureRun(report) {
@@ -680,12 +681,13 @@ function parseCardElo(provenance) {
680
681
  const match = eloEntry.reason.match(/card:\s*(\d+)/);
681
682
  return match ? parseInt(match[1], 10) : void 0;
682
683
  }
683
- function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
684
+ function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
684
685
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
685
686
  const cards = allCards.map((card) => ({
686
687
  cardId: card.cardId,
687
688
  courseId: card.courseId,
688
689
  origin: getOrigin(card),
690
+ generator: card.provenance[0]?.strategyName || card.provenance[0]?.strategy,
689
691
  finalScore: card.score,
690
692
  cardElo: parseCardElo(card.provenance),
691
693
  provenance: card.provenance,
@@ -702,6 +704,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
702
704
  generators,
703
705
  generatedCount,
704
706
  filters,
707
+ hints,
705
708
  finalCount: selectedCards.length,
706
709
  reviewsSelected,
707
710
  newSelected,
@@ -741,13 +744,169 @@ function printRunSummary(run) {
741
744
  );
742
745
  console.groupEnd();
743
746
  }
747
+ function renderUI() {
748
+ if (!_uiContainer) return;
749
+ const runs = runHistory;
750
+ const selectedRun = _selectedRunIndex !== null ? runs[_selectedRunIndex] : null;
751
+ const styles = `
752
+ #sk-pipeline-debugger {
753
+ position: fixed;
754
+ top: 0;
755
+ left: 0;
756
+ width: 100vw;
757
+ height: 100vh;
758
+ background: #f8f9fa;
759
+ color: #212529;
760
+ z-index: 999999;
761
+ display: flex;
762
+ flex-direction: column;
763
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
764
+ font-size: 14px;
765
+ }
766
+ #sk-pipeline-debugger header {
767
+ padding: 1rem;
768
+ background: #343a40;
769
+ color: white;
770
+ display: flex;
771
+ justify-content: space-between;
772
+ align-items: center;
773
+ }
774
+ #sk-pipeline-debugger .container {
775
+ display: flex;
776
+ flex: 1;
777
+ overflow: hidden;
778
+ }
779
+ #sk-pipeline-debugger .sidebar {
780
+ width: 300px;
781
+ border-right: 1px solid #dee2e6;
782
+ overflow-y: auto;
783
+ background: white;
784
+ }
785
+ #sk-pipeline-debugger .main-content {
786
+ flex: 1;
787
+ overflow-y: auto;
788
+ padding: 1.5rem;
789
+ }
790
+ #sk-pipeline-debugger .run-item {
791
+ padding: 0.75rem 1rem;
792
+ border-bottom: 1px solid #eee;
793
+ cursor: pointer;
794
+ }
795
+ #sk-pipeline-debugger .run-item:hover { background: #f1f3f5; }
796
+ #sk-pipeline-debugger .run-item.active { background: #e9ecef; border-left: 4px solid #007bff; }
797
+ #sk-pipeline-debugger h2, #sk-pipeline-debugger h3 { margin-top: 0; }
798
+ #sk-pipeline-debugger table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; background: white; }
799
+ #sk-pipeline-debugger th, #sk-pipeline-debugger td { border: 1px solid #dee2e6; padding: 0.5rem; text-align: left; }
800
+ #sk-pipeline-debugger th { background: #f1f3f5; }
801
+ #sk-pipeline-debugger code { background: #f1f3f5; padding: 0.1rem 0.3rem; border-radius: 3px; font-family: monospace; }
802
+ #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
803
+ #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
804
+ #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; }
805
+ `;
806
+ const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
807
+ (r, i) => `
808
+ <div class="run-item ${i === _selectedRunIndex ? "active" : ""}" onclick="window.skuilder.pipeline._selectRun(${i})">
809
+ <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
810
+ <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
811
+ <small>${r.finalCount} cards selected</small>
812
+ </div>
813
+ `
814
+ ).join("");
815
+ let detailsHtml = '<div style="color: #6c757d; text-align: center; margin-top: 5rem;">Select a run to see details</div>';
816
+ if (selectedRun) {
817
+ const filteredCards = selectedRun.cards.filter(
818
+ (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
819
+ );
820
+ detailsHtml = `
821
+ <h2>Run: ${selectedRun.runId}</h2>
822
+ <p>
823
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
824
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
825
+ <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
826
+ </p>
827
+
828
+ <h3>Pipeline Config</h3>
829
+ <table>
830
+ <tr><th>Generator</th><td>${selectedRun.generatorName} (${selectedRun.generatedCount} candidates)</td></tr>
831
+ ${(selectedRun.generators || []).map(
832
+ (g) => `
833
+ <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>
834
+ `
835
+ ).join("")}
836
+ </table>
837
+
838
+ ${selectedRun.hints ? `
839
+ <h3>Ephemeral Hints</h3>
840
+ <table>
841
+ ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
842
+ ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
843
+ ${selectedRun.hints.boostCards ? `<tr><th>Boost Cards</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostCards, null, 2)}</pre></td></tr>` : ""}
844
+ ${selectedRun.hints.requireTags ? `<tr><th>Require Tags</th><td>${selectedRun.hints.requireTags.join(", ")}</td></tr>` : ""}
845
+ ${selectedRun.hints.requireCards ? `<tr><th>Require Cards</th><td>${selectedRun.hints.requireCards.join(", ")}</td></tr>` : ""}
846
+ ${selectedRun.hints.excludeTags ? `<tr><th>Exclude Tags</th><td>${selectedRun.hints.excludeTags.join(", ")}</td></tr>` : ""}
847
+ ${selectedRun.hints.excludeCards ? `<tr><th>Exclude Cards</th><td>${selectedRun.hints.excludeCards.join(", ")}</td></tr>` : ""}
848
+ </table>
849
+ ` : ""}
850
+
851
+ <h3>Filter Impact</h3>
852
+ <table>
853
+ <thead><tr><th>Filter</th><th>Boosted</th><th>Penalized</th><th>Passed</th><th>Removed</th></tr></thead>
854
+ <tbody>
855
+ ${selectedRun.filters.map(
856
+ (f) => `
857
+ <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>
858
+ `
859
+ ).join("")}
860
+ </tbody>
861
+ </table>
862
+
863
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
864
+ <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
865
+
866
+ <table>
867
+ <thead><tr><th>ID</th><th>Generator</th><th>Origin</th><th>Score</th><th>Selected</th></tr></thead>
868
+ <tbody>
869
+ ${filteredCards.map(
870
+ (c) => `
871
+ <tr>
872
+ <td><code>${c.cardId}</code></td>
873
+ <td>${c.generator || "unknown"}</td>
874
+ <td>${c.origin}</td>
875
+ <td>${c.finalScore.toFixed(3)}</td>
876
+ <td>${c.selected ? "\u2705" : "\u274C"}</td>
877
+ </tr>
878
+ ${c.selected || _cardSearchQuery ? `
879
+ <tr>
880
+ <td colspan="5">
881
+ <div class="provenance">${formatProvenance(c.provenance)}</div>
882
+ </td>
883
+ </tr>
884
+ ` : ""}
885
+ `
886
+ ).join("")}
887
+ </tbody>
888
+ </table>
889
+ `;
890
+ }
891
+ _uiContainer.innerHTML = `
892
+ <style>${styles}</style>
893
+ <header>
894
+ <strong>Pipeline Debugger</strong>
895
+ <button class="close-btn" onclick="window.skuilder.pipeline.ui()">Close</button>
896
+ </header>
897
+ <div class="container">
898
+ <div class="sidebar">${runListHtml}</div>
899
+ <div class="main-content">${detailsHtml}</div>
900
+ </div>
901
+ `;
902
+ }
744
903
  function mountPipelineDebugger() {
745
904
  if (typeof window === "undefined") return;
746
905
  const win = window;
747
906
  win.skuilder = win.skuilder || {};
748
907
  win.skuilder.pipeline = pipelineDebugAPI;
749
908
  }
750
- var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
909
+ var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
751
910
  var init_PipelineDebugger = __esm({
752
911
  "src/core/navigators/PipelineDebugger.ts"() {
753
912
  "use strict";
@@ -756,6 +915,9 @@ var init_PipelineDebugger = __esm({
756
915
  _activePipeline = null;
757
916
  MAX_RUNS = 10;
758
917
  runHistory = [];
918
+ _uiContainer = null;
919
+ _selectedRunIndex = null;
920
+ _cardSearchQuery = "";
759
921
  pipelineDebugAPI = {
760
922
  /**
761
923
  * Get raw run history for programmatic access.
@@ -895,16 +1057,20 @@ var init_PipelineDebugger = __esm({
895
1057
  const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
896
1058
  const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
897
1059
  const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
1060
+ const supportCard = reason.match(/supportCard=([^;]+)/)?.[1] ?? "none";
898
1061
  const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
899
1062
  const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
1063
+ const supportSource = mode === "discovered-support" ? "discovered" : mode === "support" ? "authored" : "n/a";
900
1064
  return {
901
1065
  group: parsedGroup,
902
1066
  mode,
1067
+ supportSource,
903
1068
  cardId: card.cardId,
904
1069
  selected: card.selected ? "yes" : "no",
905
1070
  finalScore: card.finalScore.toFixed(3),
906
1071
  blocked,
907
1072
  blockedTargets,
1073
+ supportCard,
908
1074
  supportTags,
909
1075
  multiplier
910
1076
  };
@@ -920,6 +1086,8 @@ var init_PipelineDebugger = __esm({
920
1086
  const selectedRows = rows.filter((r) => r.selected === "yes");
921
1087
  const blockedTargetSet = /* @__PURE__ */ new Set();
922
1088
  const supportTagSet = /* @__PURE__ */ new Set();
1089
+ const authoredSupportSet = /* @__PURE__ */ new Set();
1090
+ const discoveredSupportSet = /* @__PURE__ */ new Set();
923
1091
  for (const row of rows) {
924
1092
  if (row.blockedTargets && row.blockedTargets !== "none") {
925
1093
  row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
@@ -927,6 +1095,13 @@ var init_PipelineDebugger = __esm({
927
1095
  if (row.supportTags && row.supportTags !== "none") {
928
1096
  row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
929
1097
  }
1098
+ if (row.supportCard && row.supportCard !== "none") {
1099
+ if (row.supportSource === "discovered") {
1100
+ discoveredSupportSet.add(row.supportCard);
1101
+ } else if (row.supportSource === "authored") {
1102
+ authoredSupportSet.add(row.supportCard);
1103
+ }
1104
+ }
930
1105
  }
931
1106
  logger.info(`Prescribed cards in run: ${rows.length}`);
932
1107
  logger.info(`Selected prescribed cards: ${selectedRows.length}`);
@@ -936,6 +1111,12 @@ var init_PipelineDebugger = __esm({
936
1111
  logger.info(
937
1112
  `Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
938
1113
  );
1114
+ logger.info(
1115
+ `Authored support cards emitted: ${authoredSupportSet.size > 0 ? [...authoredSupportSet].join(", ") : "none"}`
1116
+ );
1117
+ logger.info(
1118
+ `Discovered support cards emitted: ${discoveredSupportSet.size > 0 ? [...discoveredSupportSet].join(", ") : "none"}`
1119
+ );
939
1120
  console.groupEnd();
940
1121
  },
941
1122
  /**
@@ -1070,6 +1251,39 @@ var init_PipelineDebugger = __esm({
1070
1251
  Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
1071
1252
  );
1072
1253
  },
1254
+ /**
1255
+ * Toggle the full-screen UI debugger.
1256
+ */
1257
+ ui() {
1258
+ if (_uiContainer) {
1259
+ document.body.removeChild(_uiContainer);
1260
+ _uiContainer = null;
1261
+ return;
1262
+ }
1263
+ _uiContainer = document.createElement("div");
1264
+ _uiContainer.id = "sk-pipeline-debugger";
1265
+ document.body.appendChild(_uiContainer);
1266
+ if (_selectedRunIndex === null && runHistory.length > 0) {
1267
+ _selectedRunIndex = 0;
1268
+ }
1269
+ renderUI();
1270
+ },
1271
+ /**
1272
+ * Internal UI helpers
1273
+ * @internal
1274
+ */
1275
+ _selectRun(index) {
1276
+ _selectedRunIndex = index;
1277
+ renderUI();
1278
+ },
1279
+ /**
1280
+ * Internal UI helpers
1281
+ * @internal
1282
+ */
1283
+ _setSearch(query) {
1284
+ _cardSearchQuery = query;
1285
+ renderUI();
1286
+ },
1073
1287
  /**
1074
1288
  * Show help.
1075
1289
  */
@@ -1078,6 +1292,7 @@ var init_PipelineDebugger = __esm({
1078
1292
  \u{1F527} Pipeline Debug API
1079
1293
 
1080
1294
  Commands:
1295
+ .ui() Toggle full-screen UI debugger
1081
1296
  .showLastRun() Show summary of most recent pipeline run
1082
1297
  .showRun(id|index) Show summary of a specific run (by index or ID suffix)
1083
1298
  .showCard(cardId) Show provenance trail for a specific card
@@ -1094,6 +1309,7 @@ Commands:
1094
1309
  .help() Show this help message
1095
1310
 
1096
1311
  Example:
1312
+ window.skuilder.pipeline.ui()
1097
1313
  window.skuilder.pipeline.showLastRun()
1098
1314
  window.skuilder.pipeline.showRun(1)
1099
1315
  await window.skuilder.pipeline.diagnoseCardSpace()
@@ -1256,7 +1472,7 @@ var init_CompositeGenerator = __esm({
1256
1472
  for (const [, items] of byCardId) {
1257
1473
  const cards2 = items.map((i) => i.card);
1258
1474
  const aggregatedScore = this.aggregateScores(items);
1259
- const finalScore = Math.min(1, aggregatedScore);
1475
+ const finalScore = Math.max(0, aggregatedScore);
1260
1476
  const mergedProvenance = cards2.flatMap((c) => c.provenance);
1261
1477
  const initialScore = cards2[0].score;
1262
1478
  const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
@@ -1453,10 +1669,26 @@ function matchesTagPattern(tag, pattern) {
1453
1669
  const re = new RegExp(`^${escaped}$`);
1454
1670
  return re.test(tag);
1455
1671
  }
1672
+ function extractWordStem(cardId) {
1673
+ for (const prefix of ["c-ml-", "c-ws-", "c-spelling-"]) {
1674
+ if (cardId.startsWith(prefix)) {
1675
+ const rest = cardId.slice(prefix.length);
1676
+ const lastDash = rest.lastIndexOf("-");
1677
+ return lastDash > 0 ? rest.slice(0, lastDash) : rest;
1678
+ }
1679
+ }
1680
+ return cardId;
1681
+ }
1682
+ function shuffleInPlace(arr) {
1683
+ for (let i = arr.length - 1; i > 0; i--) {
1684
+ const j = Math.floor(Math.random() * (i + 1));
1685
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1686
+ }
1687
+ }
1456
1688
  function pickTopByScore(cards, limit) {
1457
1689
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1458
1690
  }
1459
- 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;
1691
+ 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;
1460
1692
  var init_prescribed = __esm({
1461
1693
  "src/core/navigators/generators/prescribed.ts"() {
1462
1694
  "use strict";
@@ -1469,11 +1701,10 @@ var init_prescribed = __esm({
1469
1701
  DEFAULT_MIN_COUNT = 3;
1470
1702
  BASE_TARGET_SCORE = 1;
1471
1703
  BASE_SUPPORT_SCORE = 0.8;
1704
+ DISCOVERED_SUPPORT_SCORE = 12;
1472
1705
  MAX_TARGET_MULTIPLIER = 8;
1473
1706
  MAX_SUPPORT_MULTIPLIER = 4;
1474
- LOCKED_TAG_PREFIXES = ["concept:"];
1475
- LESSON_GATE_PENALTY_TAG_HINT = "concept:";
1476
- PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
1707
+ PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
1477
1708
  PrescribedCardsGenerator = class extends ContentNavigator {
1478
1709
  name;
1479
1710
  config;
@@ -1509,6 +1740,20 @@ var init_prescribed = __esm({
1509
1740
  const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
1510
1741
  const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
1511
1742
  const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
1743
+ const courseTagDocs = await this.course.getCourseTagStubs().catch(
1744
+ () => ({
1745
+ rows: [],
1746
+ offset: 0,
1747
+ total_rows: 0
1748
+ })
1749
+ );
1750
+ const cardsByTag = /* @__PURE__ */ new Map();
1751
+ for (const row of courseTagDocs.rows ?? []) {
1752
+ const tagDoc = row.doc;
1753
+ if (tagDoc?.name && Array.isArray(tagDoc.taggedCards)) {
1754
+ cardsByTag.set(tagDoc.name, [...tagDoc.taggedCards]);
1755
+ }
1756
+ }
1512
1757
  const nextState = {
1513
1758
  updatedAt: isoNow(),
1514
1759
  groups: {}
@@ -1523,11 +1768,31 @@ var init_prescribed = __esm({
1523
1768
  activeIds,
1524
1769
  seenIds,
1525
1770
  tagsByCard,
1771
+ cardsByTag,
1526
1772
  hierarchyConfigs,
1527
1773
  userTagElo,
1528
1774
  userGlobalElo
1529
1775
  });
1530
1776
  groupRuntimes.push(runtime);
1777
+ logger.info(
1778
+ `[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)}`
1779
+ );
1780
+ if (runtime.blockedTargets.length > 0) {
1781
+ logger.info(
1782
+ `[Prescribed] Group '${group.id}' blocked targets: ${runtime.blockedTargets.join(", ")}`
1783
+ );
1784
+ logger.info(
1785
+ `[Prescribed] Group '${group.id}' support tags needed: ${runtime.supportTags.join(", ") || "(none)"}`
1786
+ );
1787
+ logger.info(
1788
+ `[Prescribed] Group '${group.id}' escalation mode: ` + (runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support-candidates" : "boost-only")
1789
+ );
1790
+ if (runtime.discoveredSupportCandidates.length > 0) {
1791
+ logger.info(
1792
+ `[Prescribed] Group '${group.id}' discovered support candidates: ${runtime.discoveredSupportCandidates.join(", ")}`
1793
+ );
1794
+ }
1795
+ }
1531
1796
  nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
1532
1797
  const directCards = this.buildDirectTargetCards(
1533
1798
  runtime,
@@ -1539,15 +1804,30 @@ var init_prescribed = __esm({
1539
1804
  courseId,
1540
1805
  emittedIds
1541
1806
  );
1542
- emitted.push(...directCards, ...supportCards);
1807
+ const discoveredSupportCards = this.buildDiscoveredSupportCards(
1808
+ runtime,
1809
+ courseId,
1810
+ emittedIds
1811
+ );
1812
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
1543
1813
  }
1544
1814
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1545
1815
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
1546
1816
  boostTags: hintSummary.boostTags,
1547
1817
  _label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
1548
1818
  } : void 0;
1819
+ if (hints) {
1820
+ const tagEntries = Object.entries(hints.boostTags ?? {});
1821
+ logger.info(
1822
+ `[Prescribed] Emitting ${tagEntries.length} boost hint(s): ` + tagEntries.map(([tag, mult]) => `${tag}\xD7${mult.toFixed(1)}`).join(", ")
1823
+ );
1824
+ } else {
1825
+ logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
1826
+ }
1549
1827
  if (emitted.length === 0) {
1550
- logger.debug("[Prescribed] No prescribed targets/support emitted this run");
1828
+ logger.info(
1829
+ "[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
1830
+ );
1551
1831
  await this.putStrategyState(nextState).catch((e) => {
1552
1832
  logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
1553
1833
  });
@@ -1580,7 +1860,7 @@ var init_prescribed = __esm({
1580
1860
  logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
1581
1861
  });
1582
1862
  logger.info(
1583
- `[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)`
1863
+ `[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)`
1584
1864
  );
1585
1865
  return hints ? { cards: finalCards, hints } : { cards: finalCards };
1586
1866
  }
@@ -1610,9 +1890,15 @@ var init_prescribed = __esm({
1610
1890
  const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
1611
1891
  const groups = groupsRaw.map((raw, i) => ({
1612
1892
  id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
1613
- targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
1614
- supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
1615
- supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
1893
+ targetCardIds: dedupe(
1894
+ Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []
1895
+ ),
1896
+ supportCardIds: dedupe(
1897
+ Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []
1898
+ ),
1899
+ supportTagPatterns: dedupe(
1900
+ Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []
1901
+ ),
1616
1902
  freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
1617
1903
  maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
1618
1904
  maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
@@ -1629,7 +1915,7 @@ var init_prescribed = __esm({
1629
1915
  }
1630
1916
  async loadHierarchyConfigs() {
1631
1917
  try {
1632
- const strategies = await this.course.getNavigationStrategies();
1918
+ const strategies = await this.course.getAllNavigationStrategies();
1633
1919
  return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
1634
1920
  try {
1635
1921
  const parsed = JSON.parse(s.serializedData);
@@ -1652,6 +1938,7 @@ var init_prescribed = __esm({
1652
1938
  activeIds,
1653
1939
  seenIds,
1654
1940
  tagsByCard,
1941
+ cardsByTag,
1655
1942
  hierarchyConfigs,
1656
1943
  userTagElo,
1657
1944
  userGlobalElo
@@ -1715,6 +2002,22 @@ var init_prescribed = __esm({
1715
2002
  [...supportTags]
1716
2003
  )
1717
2004
  ]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
2005
+ const discoveredSupportCandidates = blockedTargets.length > 0 && supportTags.size > 0 && supportCandidates.length === 0 ? this.findDiscoveredSupportCards({
2006
+ supportTags: [...supportTags],
2007
+ cardsByTag,
2008
+ activeIds,
2009
+ seenIds,
2010
+ excludedIds: /* @__PURE__ */ new Set([
2011
+ ...group.targetCardIds,
2012
+ ...group.supportCardIds ?? []
2013
+ ]),
2014
+ limit: group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN
2015
+ }) : [];
2016
+ if (blockedTargets.length > 0 && supportTags.size > 0 && discoveredSupportCandidates.length === 0) {
2017
+ logger.info(
2018
+ `[Prescribed] Group '${group.id}' discovered 0 broader support candidates (blocked=${blockedTargets.length}; authoredSupport=${supportCandidates.length})`
2019
+ );
2020
+ }
1718
2021
  const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
1719
2022
  const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
1720
2023
  const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
@@ -1728,6 +2031,7 @@ var init_prescribed = __esm({
1728
2031
  surfaceableTargets,
1729
2032
  targetTags,
1730
2033
  supportCandidates,
2034
+ discoveredSupportCandidates,
1731
2035
  supportTags: [...supportTags],
1732
2036
  pressureMultiplier,
1733
2037
  supportMultiplier,
@@ -1798,6 +2102,33 @@ var init_prescribed = __esm({
1798
2102
  }
1799
2103
  return cards;
1800
2104
  }
2105
+ buildDiscoveredSupportCards(runtime, courseId, emittedIds) {
2106
+ if (runtime.blockedTargets.length === 0 || runtime.discoveredSupportCandidates.length === 0) {
2107
+ return [];
2108
+ }
2109
+ const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
2110
+ const supportIds = runtime.discoveredSupportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
2111
+ const cards = [];
2112
+ for (const cardId of supportIds) {
2113
+ emittedIds.add(cardId);
2114
+ cards.push({
2115
+ cardId,
2116
+ courseId,
2117
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
2118
+ provenance: [
2119
+ {
2120
+ strategy: "prescribed",
2121
+ strategyName: this.strategyName || this.name,
2122
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2123
+ action: "generated",
2124
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
2125
+ 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}`
2126
+ }
2127
+ ]
2128
+ });
2129
+ }
2130
+ return cards;
2131
+ }
1801
2132
  findSupportCardsByTags(group, tagsByCard, supportTags) {
1802
2133
  if (supportTags.length === 0) {
1803
2134
  return [];
@@ -1820,6 +2151,40 @@ var init_prescribed = __esm({
1820
2151
  }
1821
2152
  return [...candidates];
1822
2153
  }
2154
+ findDiscoveredSupportCards(args) {
2155
+ const { supportTags, cardsByTag, activeIds, seenIds, excludedIds, limit } = args;
2156
+ const byCardId = /* @__PURE__ */ new Map();
2157
+ for (const supportTag of supportTags) {
2158
+ const taggedCards = cardsByTag.get(supportTag) ?? [];
2159
+ for (const cardId of taggedCards) {
2160
+ if (activeIds.has(cardId) || seenIds.has(cardId) || excludedIds.has(cardId)) {
2161
+ continue;
2162
+ }
2163
+ const existing = byCardId.get(cardId);
2164
+ if (existing) {
2165
+ existing.matches += 1;
2166
+ } else {
2167
+ byCardId.set(cardId, { cardId, matches: 1 });
2168
+ }
2169
+ }
2170
+ }
2171
+ const candidates = [...byCardId.values()].sort((a, b) => b.matches - a.matches || a.cardId.localeCompare(b.cardId));
2172
+ const usedStems = /* @__PURE__ */ new Set();
2173
+ const diverse = [];
2174
+ const deferred = [];
2175
+ for (const entry of candidates) {
2176
+ const stem = extractWordStem(entry.cardId);
2177
+ if (!usedStems.has(stem)) {
2178
+ usedStems.add(stem);
2179
+ diverse.push(entry);
2180
+ } else {
2181
+ deferred.push(entry);
2182
+ }
2183
+ }
2184
+ shuffleInPlace(diverse);
2185
+ shuffleInPlace(deferred);
2186
+ return [...diverse, ...deferred].slice(0, limit).map((entry) => entry.cardId);
2187
+ }
1823
2188
  resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
1824
2189
  const supportTags = /* @__PURE__ */ new Set();
1825
2190
  let blocked = false;
@@ -1865,7 +2230,6 @@ var init_prescribed = __esm({
1865
2230
  }
1866
2231
  collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
1867
2232
  if (depth < 0 || visited.has(tag)) return;
1868
- if (this.isHardGatedTag(tag)) return;
1869
2233
  visited.add(tag);
1870
2234
  let walkedFurther = false;
1871
2235
  for (const hierarchy of hierarchyConfigs) {
@@ -1893,9 +2257,6 @@ var init_prescribed = __esm({
1893
2257
  out.add(tag);
1894
2258
  }
1895
2259
  }
1896
- isHardGatedTag(tag) {
1897
- return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
1898
- }
1899
2260
  isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
1900
2261
  if (!userTagElo) return false;
1901
2262
  const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
@@ -3441,6 +3802,32 @@ var init_Pipeline = __esm({
3441
3802
  cards = await this.hydrateTags(cards);
3442
3803
  const tHydrate = performance.now();
3443
3804
  const allCardsBeforeFiltering = [...cards];
3805
+ const pendingHints = this._ephemeralHints;
3806
+ if (pendingHints?.requireCards?.length) {
3807
+ const poolIds = new Set(allCardsBeforeFiltering.map((c) => c.cardId));
3808
+ const missingIds = pendingHints.requireCards.filter(
3809
+ (p) => !p.includes("*") && !poolIds.has(p)
3810
+ );
3811
+ if (missingIds.length > 0) {
3812
+ const fetchedTags = await this.course.getAppliedTagsBatch(missingIds);
3813
+ const courseId = this.course.getCourseID();
3814
+ for (const cardId of missingIds) {
3815
+ allCardsBeforeFiltering.push({
3816
+ cardId,
3817
+ courseId,
3818
+ score: 1,
3819
+ tags: fetchedTags.get(cardId) ?? [],
3820
+ provenance: []
3821
+ });
3822
+ }
3823
+ logger.info(
3824
+ `[Pipeline] Pre-fetched ${missingIds.length} required card(s) into pool: ${missingIds.join(", ")}`
3825
+ );
3826
+ }
3827
+ }
3828
+ const prescribedIds = new Set(
3829
+ cards.filter((c) => c.provenance.some((p) => p.strategy === "prescribed")).map((c) => c.cardId)
3830
+ );
3444
3831
  const filterImpacts = [];
3445
3832
  for (const filter of this.filters) {
3446
3833
  const beforeCount = cards.length;
@@ -3455,6 +3842,17 @@ var init_Pipeline = __esm({
3455
3842
  else passed++;
3456
3843
  }
3457
3844
  filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
3845
+ if (prescribedIds.size > 0) {
3846
+ const survivingIds = new Set(cards.map((c) => c.cardId));
3847
+ const killedPrescribed = [...prescribedIds].filter((id) => !survivingIds.has(id));
3848
+ const zeroedPrescribed = cards.filter((c) => prescribedIds.has(c.cardId) && c.score === 0).map((c) => c.cardId);
3849
+ if (killedPrescribed.length > 0 || zeroedPrescribed.length > 0) {
3850
+ logger.info(
3851
+ `[Pipeline] Filter '${filter.name}' impact on prescribed cards: ` + (killedPrescribed.length > 0 ? `removed=[${killedPrescribed.join(", ")}] ` : "") + (zeroedPrescribed.length > 0 ? `zeroed=[${zeroedPrescribed.join(", ")}]` : "")
3852
+ );
3853
+ killedPrescribed.forEach((id) => prescribedIds.delete(id));
3854
+ }
3855
+ }
3458
3856
  logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
3459
3857
  }
3460
3858
  cards = cards.filter((c) => c.score > 0);
@@ -3491,7 +3889,8 @@ var init_Pipeline = __esm({
3491
3889
  filterImpacts,
3492
3890
  cards,
3493
3891
  result,
3494
- context.userElo
3892
+ context.userElo,
3893
+ hints ?? void 0
3495
3894
  );
3496
3895
  captureRun(report);
3497
3896
  } catch (e) {
@@ -3601,13 +4000,27 @@ var init_Pipeline = __esm({
3601
4000
  }
3602
4001
  }
3603
4002
  const cardIds = new Set(cards.map((c) => c.cardId));
4003
+ const cardMap = new Map(cards.map((c) => [c.cardId, c]));
3604
4004
  const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
3605
- const inject = (card, reason) => {
3606
- if (!cardIds.has(card.cardId)) {
3607
- const floorScore = Math.max(card.score, 1);
4005
+ const applyRequirement = (card, reason) => {
4006
+ const mandatoryScore = Number.POSITIVE_INFINITY;
4007
+ const existing = cardMap.get(card.cardId);
4008
+ if (existing) {
4009
+ if (existing.score < mandatoryScore) {
4010
+ existing.score = mandatoryScore;
4011
+ existing.provenance.push({
4012
+ strategy: "ephemeralHint",
4013
+ strategyId: "ephemeral-hint",
4014
+ strategyName: hintLabel,
4015
+ action: "boosted",
4016
+ score: mandatoryScore,
4017
+ reason: `${reason} (upgrade to mandatory score)`
4018
+ });
4019
+ }
4020
+ } else {
3608
4021
  cards.push({
3609
4022
  ...card,
3610
- score: floorScore,
4023
+ score: mandatoryScore,
3611
4024
  provenance: [
3612
4025
  ...card.provenance,
3613
4026
  {
@@ -3615,25 +4028,41 @@ var init_Pipeline = __esm({
3615
4028
  strategyId: "ephemeral-hint",
3616
4029
  strategyName: hintLabel,
3617
4030
  action: "boosted",
3618
- score: floorScore,
4031
+ score: mandatoryScore,
3619
4032
  reason
3620
4033
  }
3621
4034
  ]
3622
4035
  });
3623
4036
  cardIds.add(card.cardId);
4037
+ cardMap.set(card.cardId, cards[cards.length - 1]);
3624
4038
  }
3625
4039
  };
3626
4040
  if (hints.requireCards?.length) {
3627
4041
  for (const pattern of hints.requireCards) {
4042
+ for (const cardId of cardIds) {
4043
+ if (globMatch(cardId, pattern)) {
4044
+ applyRequirement(cardMap.get(cardId), `requireCard ${pattern}`);
4045
+ }
4046
+ }
3628
4047
  for (const card of allCards) {
3629
- if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
4048
+ if (globMatch(card.cardId, pattern)) {
4049
+ applyRequirement(card, `requireCard ${pattern}`);
4050
+ }
3630
4051
  }
3631
4052
  }
3632
4053
  }
3633
4054
  if (hints.requireTags?.length) {
3634
4055
  for (const pattern of hints.requireTags) {
4056
+ for (const cardId of cardIds) {
4057
+ const card = cardMap.get(cardId);
4058
+ if (cardMatchesTagPattern(card, pattern)) {
4059
+ applyRequirement(card, `requireTag ${pattern}`);
4060
+ }
4061
+ }
3635
4062
  for (const card of allCards) {
3636
- if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
4063
+ if (cardMatchesTagPattern(card, pattern)) {
4064
+ applyRequirement(card, `requireTag ${pattern}`);
4065
+ }
3637
4066
  }
3638
4067
  }
3639
4068
  }