@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.
@@ -637,8 +637,9 @@ function getOrigin(card) {
637
637
  const firstEntry = card.provenance[0];
638
638
  if (!firstEntry) return "unknown";
639
639
  const reason = firstEntry.reason?.toLowerCase() || "";
640
- if (reason.includes("new card")) return "new";
641
- if (reason.includes("review")) return "review";
640
+ const strategy = firstEntry.strategy?.toLowerCase() || "";
641
+ if (reason.includes("new card") || strategy.includes("elo")) return "new";
642
+ if (reason.includes("review") || strategy.includes("srs")) return "review";
642
643
  return "unknown";
643
644
  }
644
645
  function captureRun(report) {
@@ -658,12 +659,13 @@ function parseCardElo(provenance) {
658
659
  const match = eloEntry.reason.match(/card:\s*(\d+)/);
659
660
  return match ? parseInt(match[1], 10) : void 0;
660
661
  }
661
- function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
662
+ function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
662
663
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
663
664
  const cards = allCards.map((card) => ({
664
665
  cardId: card.cardId,
665
666
  courseId: card.courseId,
666
667
  origin: getOrigin(card),
668
+ generator: card.provenance[0]?.strategyName || card.provenance[0]?.strategy,
667
669
  finalScore: card.score,
668
670
  cardElo: parseCardElo(card.provenance),
669
671
  provenance: card.provenance,
@@ -680,6 +682,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
680
682
  generators,
681
683
  generatedCount,
682
684
  filters,
685
+ hints,
683
686
  finalCount: selectedCards.length,
684
687
  reviewsSelected,
685
688
  newSelected,
@@ -719,13 +722,169 @@ function printRunSummary(run) {
719
722
  );
720
723
  console.groupEnd();
721
724
  }
725
+ function renderUI() {
726
+ if (!_uiContainer) return;
727
+ const runs = runHistory;
728
+ const selectedRun = _selectedRunIndex !== null ? runs[_selectedRunIndex] : null;
729
+ const styles = `
730
+ #sk-pipeline-debugger {
731
+ position: fixed;
732
+ top: 0;
733
+ left: 0;
734
+ width: 100vw;
735
+ height: 100vh;
736
+ background: #f8f9fa;
737
+ color: #212529;
738
+ z-index: 999999;
739
+ display: flex;
740
+ flex-direction: column;
741
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
742
+ font-size: 14px;
743
+ }
744
+ #sk-pipeline-debugger header {
745
+ padding: 1rem;
746
+ background: #343a40;
747
+ color: white;
748
+ display: flex;
749
+ justify-content: space-between;
750
+ align-items: center;
751
+ }
752
+ #sk-pipeline-debugger .container {
753
+ display: flex;
754
+ flex: 1;
755
+ overflow: hidden;
756
+ }
757
+ #sk-pipeline-debugger .sidebar {
758
+ width: 300px;
759
+ border-right: 1px solid #dee2e6;
760
+ overflow-y: auto;
761
+ background: white;
762
+ }
763
+ #sk-pipeline-debugger .main-content {
764
+ flex: 1;
765
+ overflow-y: auto;
766
+ padding: 1.5rem;
767
+ }
768
+ #sk-pipeline-debugger .run-item {
769
+ padding: 0.75rem 1rem;
770
+ border-bottom: 1px solid #eee;
771
+ cursor: pointer;
772
+ }
773
+ #sk-pipeline-debugger .run-item:hover { background: #f1f3f5; }
774
+ #sk-pipeline-debugger .run-item.active { background: #e9ecef; border-left: 4px solid #007bff; }
775
+ #sk-pipeline-debugger h2, #sk-pipeline-debugger h3 { margin-top: 0; }
776
+ #sk-pipeline-debugger table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; background: white; }
777
+ #sk-pipeline-debugger th, #sk-pipeline-debugger td { border: 1px solid #dee2e6; padding: 0.5rem; text-align: left; }
778
+ #sk-pipeline-debugger th { background: #f1f3f5; }
779
+ #sk-pipeline-debugger code { background: #f1f3f5; padding: 0.1rem 0.3rem; border-radius: 3px; font-family: monospace; }
780
+ #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
781
+ #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
782
+ #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; }
783
+ `;
784
+ const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
785
+ (r, i) => `
786
+ <div class="run-item ${i === _selectedRunIndex ? "active" : ""}" onclick="window.skuilder.pipeline._selectRun(${i})">
787
+ <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
788
+ <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
789
+ <small>${r.finalCount} cards selected</small>
790
+ </div>
791
+ `
792
+ ).join("");
793
+ let detailsHtml = '<div style="color: #6c757d; text-align: center; margin-top: 5rem;">Select a run to see details</div>';
794
+ if (selectedRun) {
795
+ const filteredCards = selectedRun.cards.filter(
796
+ (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
797
+ );
798
+ detailsHtml = `
799
+ <h2>Run: ${selectedRun.runId}</h2>
800
+ <p>
801
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
802
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
803
+ <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
804
+ </p>
805
+
806
+ <h3>Pipeline Config</h3>
807
+ <table>
808
+ <tr><th>Generator</th><td>${selectedRun.generatorName} (${selectedRun.generatedCount} candidates)</td></tr>
809
+ ${(selectedRun.generators || []).map(
810
+ (g) => `
811
+ <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>
812
+ `
813
+ ).join("")}
814
+ </table>
815
+
816
+ ${selectedRun.hints ? `
817
+ <h3>Ephemeral Hints</h3>
818
+ <table>
819
+ ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
820
+ ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
821
+ ${selectedRun.hints.boostCards ? `<tr><th>Boost Cards</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostCards, null, 2)}</pre></td></tr>` : ""}
822
+ ${selectedRun.hints.requireTags ? `<tr><th>Require Tags</th><td>${selectedRun.hints.requireTags.join(", ")}</td></tr>` : ""}
823
+ ${selectedRun.hints.requireCards ? `<tr><th>Require Cards</th><td>${selectedRun.hints.requireCards.join(", ")}</td></tr>` : ""}
824
+ ${selectedRun.hints.excludeTags ? `<tr><th>Exclude Tags</th><td>${selectedRun.hints.excludeTags.join(", ")}</td></tr>` : ""}
825
+ ${selectedRun.hints.excludeCards ? `<tr><th>Exclude Cards</th><td>${selectedRun.hints.excludeCards.join(", ")}</td></tr>` : ""}
826
+ </table>
827
+ ` : ""}
828
+
829
+ <h3>Filter Impact</h3>
830
+ <table>
831
+ <thead><tr><th>Filter</th><th>Boosted</th><th>Penalized</th><th>Passed</th><th>Removed</th></tr></thead>
832
+ <tbody>
833
+ ${selectedRun.filters.map(
834
+ (f) => `
835
+ <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>
836
+ `
837
+ ).join("")}
838
+ </tbody>
839
+ </table>
840
+
841
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
842
+ <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
843
+
844
+ <table>
845
+ <thead><tr><th>ID</th><th>Generator</th><th>Origin</th><th>Score</th><th>Selected</th></tr></thead>
846
+ <tbody>
847
+ ${filteredCards.map(
848
+ (c) => `
849
+ <tr>
850
+ <td><code>${c.cardId}</code></td>
851
+ <td>${c.generator || "unknown"}</td>
852
+ <td>${c.origin}</td>
853
+ <td>${c.finalScore.toFixed(3)}</td>
854
+ <td>${c.selected ? "\u2705" : "\u274C"}</td>
855
+ </tr>
856
+ ${c.selected || _cardSearchQuery ? `
857
+ <tr>
858
+ <td colspan="5">
859
+ <div class="provenance">${formatProvenance(c.provenance)}</div>
860
+ </td>
861
+ </tr>
862
+ ` : ""}
863
+ `
864
+ ).join("")}
865
+ </tbody>
866
+ </table>
867
+ `;
868
+ }
869
+ _uiContainer.innerHTML = `
870
+ <style>${styles}</style>
871
+ <header>
872
+ <strong>Pipeline Debugger</strong>
873
+ <button class="close-btn" onclick="window.skuilder.pipeline.ui()">Close</button>
874
+ </header>
875
+ <div class="container">
876
+ <div class="sidebar">${runListHtml}</div>
877
+ <div class="main-content">${detailsHtml}</div>
878
+ </div>
879
+ `;
880
+ }
722
881
  function mountPipelineDebugger() {
723
882
  if (typeof window === "undefined") return;
724
883
  const win = window;
725
884
  win.skuilder = win.skuilder || {};
726
885
  win.skuilder.pipeline = pipelineDebugAPI;
727
886
  }
728
- var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
887
+ var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
729
888
  var init_PipelineDebugger = __esm({
730
889
  "src/core/navigators/PipelineDebugger.ts"() {
731
890
  "use strict";
@@ -734,6 +893,9 @@ var init_PipelineDebugger = __esm({
734
893
  _activePipeline = null;
735
894
  MAX_RUNS = 10;
736
895
  runHistory = [];
896
+ _uiContainer = null;
897
+ _selectedRunIndex = null;
898
+ _cardSearchQuery = "";
737
899
  pipelineDebugAPI = {
738
900
  /**
739
901
  * Get raw run history for programmatic access.
@@ -873,16 +1035,20 @@ var init_PipelineDebugger = __esm({
873
1035
  const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
874
1036
  const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
875
1037
  const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
1038
+ const supportCard = reason.match(/supportCard=([^;]+)/)?.[1] ?? "none";
876
1039
  const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
877
1040
  const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
1041
+ const supportSource = mode === "discovered-support" ? "discovered" : mode === "support" ? "authored" : "n/a";
878
1042
  return {
879
1043
  group: parsedGroup,
880
1044
  mode,
1045
+ supportSource,
881
1046
  cardId: card.cardId,
882
1047
  selected: card.selected ? "yes" : "no",
883
1048
  finalScore: card.finalScore.toFixed(3),
884
1049
  blocked,
885
1050
  blockedTargets,
1051
+ supportCard,
886
1052
  supportTags,
887
1053
  multiplier
888
1054
  };
@@ -898,6 +1064,8 @@ var init_PipelineDebugger = __esm({
898
1064
  const selectedRows = rows.filter((r) => r.selected === "yes");
899
1065
  const blockedTargetSet = /* @__PURE__ */ new Set();
900
1066
  const supportTagSet = /* @__PURE__ */ new Set();
1067
+ const authoredSupportSet = /* @__PURE__ */ new Set();
1068
+ const discoveredSupportSet = /* @__PURE__ */ new Set();
901
1069
  for (const row of rows) {
902
1070
  if (row.blockedTargets && row.blockedTargets !== "none") {
903
1071
  row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
@@ -905,6 +1073,13 @@ var init_PipelineDebugger = __esm({
905
1073
  if (row.supportTags && row.supportTags !== "none") {
906
1074
  row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
907
1075
  }
1076
+ if (row.supportCard && row.supportCard !== "none") {
1077
+ if (row.supportSource === "discovered") {
1078
+ discoveredSupportSet.add(row.supportCard);
1079
+ } else if (row.supportSource === "authored") {
1080
+ authoredSupportSet.add(row.supportCard);
1081
+ }
1082
+ }
908
1083
  }
909
1084
  logger.info(`Prescribed cards in run: ${rows.length}`);
910
1085
  logger.info(`Selected prescribed cards: ${selectedRows.length}`);
@@ -914,6 +1089,12 @@ var init_PipelineDebugger = __esm({
914
1089
  logger.info(
915
1090
  `Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
916
1091
  );
1092
+ logger.info(
1093
+ `Authored support cards emitted: ${authoredSupportSet.size > 0 ? [...authoredSupportSet].join(", ") : "none"}`
1094
+ );
1095
+ logger.info(
1096
+ `Discovered support cards emitted: ${discoveredSupportSet.size > 0 ? [...discoveredSupportSet].join(", ") : "none"}`
1097
+ );
917
1098
  console.groupEnd();
918
1099
  },
919
1100
  /**
@@ -1048,6 +1229,39 @@ var init_PipelineDebugger = __esm({
1048
1229
  Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
1049
1230
  );
1050
1231
  },
1232
+ /**
1233
+ * Toggle the full-screen UI debugger.
1234
+ */
1235
+ ui() {
1236
+ if (_uiContainer) {
1237
+ document.body.removeChild(_uiContainer);
1238
+ _uiContainer = null;
1239
+ return;
1240
+ }
1241
+ _uiContainer = document.createElement("div");
1242
+ _uiContainer.id = "sk-pipeline-debugger";
1243
+ document.body.appendChild(_uiContainer);
1244
+ if (_selectedRunIndex === null && runHistory.length > 0) {
1245
+ _selectedRunIndex = 0;
1246
+ }
1247
+ renderUI();
1248
+ },
1249
+ /**
1250
+ * Internal UI helpers
1251
+ * @internal
1252
+ */
1253
+ _selectRun(index) {
1254
+ _selectedRunIndex = index;
1255
+ renderUI();
1256
+ },
1257
+ /**
1258
+ * Internal UI helpers
1259
+ * @internal
1260
+ */
1261
+ _setSearch(query) {
1262
+ _cardSearchQuery = query;
1263
+ renderUI();
1264
+ },
1051
1265
  /**
1052
1266
  * Show help.
1053
1267
  */
@@ -1056,6 +1270,7 @@ var init_PipelineDebugger = __esm({
1056
1270
  \u{1F527} Pipeline Debug API
1057
1271
 
1058
1272
  Commands:
1273
+ .ui() Toggle full-screen UI debugger
1059
1274
  .showLastRun() Show summary of most recent pipeline run
1060
1275
  .showRun(id|index) Show summary of a specific run (by index or ID suffix)
1061
1276
  .showCard(cardId) Show provenance trail for a specific card
@@ -1072,6 +1287,7 @@ Commands:
1072
1287
  .help() Show this help message
1073
1288
 
1074
1289
  Example:
1290
+ window.skuilder.pipeline.ui()
1075
1291
  window.skuilder.pipeline.showLastRun()
1076
1292
  window.skuilder.pipeline.showRun(1)
1077
1293
  await window.skuilder.pipeline.diagnoseCardSpace()
@@ -1234,7 +1450,7 @@ var init_CompositeGenerator = __esm({
1234
1450
  for (const [, items] of byCardId) {
1235
1451
  const cards2 = items.map((i) => i.card);
1236
1452
  const aggregatedScore = this.aggregateScores(items);
1237
- const finalScore = Math.min(1, aggregatedScore);
1453
+ const finalScore = Math.max(0, aggregatedScore);
1238
1454
  const mergedProvenance = cards2.flatMap((c) => c.provenance);
1239
1455
  const initialScore = cards2[0].score;
1240
1456
  const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
@@ -1431,10 +1647,26 @@ function matchesTagPattern(tag, pattern) {
1431
1647
  const re = new RegExp(`^${escaped}$`);
1432
1648
  return re.test(tag);
1433
1649
  }
1650
+ function extractWordStem(cardId) {
1651
+ for (const prefix of ["c-ml-", "c-ws-", "c-spelling-"]) {
1652
+ if (cardId.startsWith(prefix)) {
1653
+ const rest = cardId.slice(prefix.length);
1654
+ const lastDash = rest.lastIndexOf("-");
1655
+ return lastDash > 0 ? rest.slice(0, lastDash) : rest;
1656
+ }
1657
+ }
1658
+ return cardId;
1659
+ }
1660
+ function shuffleInPlace(arr) {
1661
+ for (let i = arr.length - 1; i > 0; i--) {
1662
+ const j = Math.floor(Math.random() * (i + 1));
1663
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1664
+ }
1665
+ }
1434
1666
  function pickTopByScore(cards, limit) {
1435
1667
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1436
1668
  }
1437
- 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;
1669
+ 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;
1438
1670
  var init_prescribed = __esm({
1439
1671
  "src/core/navigators/generators/prescribed.ts"() {
1440
1672
  "use strict";
@@ -1447,11 +1679,10 @@ var init_prescribed = __esm({
1447
1679
  DEFAULT_MIN_COUNT = 3;
1448
1680
  BASE_TARGET_SCORE = 1;
1449
1681
  BASE_SUPPORT_SCORE = 0.8;
1682
+ DISCOVERED_SUPPORT_SCORE = 12;
1450
1683
  MAX_TARGET_MULTIPLIER = 8;
1451
1684
  MAX_SUPPORT_MULTIPLIER = 4;
1452
- LOCKED_TAG_PREFIXES = ["concept:"];
1453
- LESSON_GATE_PENALTY_TAG_HINT = "concept:";
1454
- PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
1685
+ PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
1455
1686
  PrescribedCardsGenerator = class extends ContentNavigator {
1456
1687
  name;
1457
1688
  config;
@@ -1487,6 +1718,20 @@ var init_prescribed = __esm({
1487
1718
  const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
1488
1719
  const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
1489
1720
  const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
1721
+ const courseTagDocs = await this.course.getCourseTagStubs().catch(
1722
+ () => ({
1723
+ rows: [],
1724
+ offset: 0,
1725
+ total_rows: 0
1726
+ })
1727
+ );
1728
+ const cardsByTag = /* @__PURE__ */ new Map();
1729
+ for (const row of courseTagDocs.rows ?? []) {
1730
+ const tagDoc = row.doc;
1731
+ if (tagDoc?.name && Array.isArray(tagDoc.taggedCards)) {
1732
+ cardsByTag.set(tagDoc.name, [...tagDoc.taggedCards]);
1733
+ }
1734
+ }
1490
1735
  const nextState = {
1491
1736
  updatedAt: isoNow(),
1492
1737
  groups: {}
@@ -1501,11 +1746,31 @@ var init_prescribed = __esm({
1501
1746
  activeIds,
1502
1747
  seenIds,
1503
1748
  tagsByCard,
1749
+ cardsByTag,
1504
1750
  hierarchyConfigs,
1505
1751
  userTagElo,
1506
1752
  userGlobalElo
1507
1753
  });
1508
1754
  groupRuntimes.push(runtime);
1755
+ logger.info(
1756
+ `[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)}`
1757
+ );
1758
+ if (runtime.blockedTargets.length > 0) {
1759
+ logger.info(
1760
+ `[Prescribed] Group '${group.id}' blocked targets: ${runtime.blockedTargets.join(", ")}`
1761
+ );
1762
+ logger.info(
1763
+ `[Prescribed] Group '${group.id}' support tags needed: ${runtime.supportTags.join(", ") || "(none)"}`
1764
+ );
1765
+ logger.info(
1766
+ `[Prescribed] Group '${group.id}' escalation mode: ` + (runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support-candidates" : "boost-only")
1767
+ );
1768
+ if (runtime.discoveredSupportCandidates.length > 0) {
1769
+ logger.info(
1770
+ `[Prescribed] Group '${group.id}' discovered support candidates: ${runtime.discoveredSupportCandidates.join(", ")}`
1771
+ );
1772
+ }
1773
+ }
1509
1774
  nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
1510
1775
  const directCards = this.buildDirectTargetCards(
1511
1776
  runtime,
@@ -1517,15 +1782,30 @@ var init_prescribed = __esm({
1517
1782
  courseId,
1518
1783
  emittedIds
1519
1784
  );
1520
- emitted.push(...directCards, ...supportCards);
1785
+ const discoveredSupportCards = this.buildDiscoveredSupportCards(
1786
+ runtime,
1787
+ courseId,
1788
+ emittedIds
1789
+ );
1790
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
1521
1791
  }
1522
1792
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1523
1793
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
1524
1794
  boostTags: hintSummary.boostTags,
1525
1795
  _label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
1526
1796
  } : void 0;
1797
+ if (hints) {
1798
+ const tagEntries = Object.entries(hints.boostTags ?? {});
1799
+ logger.info(
1800
+ `[Prescribed] Emitting ${tagEntries.length} boost hint(s): ` + tagEntries.map(([tag, mult]) => `${tag}\xD7${mult.toFixed(1)}`).join(", ")
1801
+ );
1802
+ } else {
1803
+ logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
1804
+ }
1527
1805
  if (emitted.length === 0) {
1528
- logger.debug("[Prescribed] No prescribed targets/support emitted this run");
1806
+ logger.info(
1807
+ "[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
1808
+ );
1529
1809
  await this.putStrategyState(nextState).catch((e) => {
1530
1810
  logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
1531
1811
  });
@@ -1558,7 +1838,7 @@ var init_prescribed = __esm({
1558
1838
  logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
1559
1839
  });
1560
1840
  logger.info(
1561
- `[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)`
1841
+ `[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)`
1562
1842
  );
1563
1843
  return hints ? { cards: finalCards, hints } : { cards: finalCards };
1564
1844
  }
@@ -1588,9 +1868,15 @@ var init_prescribed = __esm({
1588
1868
  const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
1589
1869
  const groups = groupsRaw.map((raw, i) => ({
1590
1870
  id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
1591
- targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
1592
- supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
1593
- supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
1871
+ targetCardIds: dedupe(
1872
+ Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []
1873
+ ),
1874
+ supportCardIds: dedupe(
1875
+ Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []
1876
+ ),
1877
+ supportTagPatterns: dedupe(
1878
+ Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []
1879
+ ),
1594
1880
  freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
1595
1881
  maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
1596
1882
  maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
@@ -1607,7 +1893,7 @@ var init_prescribed = __esm({
1607
1893
  }
1608
1894
  async loadHierarchyConfigs() {
1609
1895
  try {
1610
- const strategies = await this.course.getNavigationStrategies();
1896
+ const strategies = await this.course.getAllNavigationStrategies();
1611
1897
  return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
1612
1898
  try {
1613
1899
  const parsed = JSON.parse(s.serializedData);
@@ -1630,6 +1916,7 @@ var init_prescribed = __esm({
1630
1916
  activeIds,
1631
1917
  seenIds,
1632
1918
  tagsByCard,
1919
+ cardsByTag,
1633
1920
  hierarchyConfigs,
1634
1921
  userTagElo,
1635
1922
  userGlobalElo
@@ -1693,6 +1980,22 @@ var init_prescribed = __esm({
1693
1980
  [...supportTags]
1694
1981
  )
1695
1982
  ]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
1983
+ const discoveredSupportCandidates = blockedTargets.length > 0 && supportTags.size > 0 && supportCandidates.length === 0 ? this.findDiscoveredSupportCards({
1984
+ supportTags: [...supportTags],
1985
+ cardsByTag,
1986
+ activeIds,
1987
+ seenIds,
1988
+ excludedIds: /* @__PURE__ */ new Set([
1989
+ ...group.targetCardIds,
1990
+ ...group.supportCardIds ?? []
1991
+ ]),
1992
+ limit: group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN
1993
+ }) : [];
1994
+ if (blockedTargets.length > 0 && supportTags.size > 0 && discoveredSupportCandidates.length === 0) {
1995
+ logger.info(
1996
+ `[Prescribed] Group '${group.id}' discovered 0 broader support candidates (blocked=${blockedTargets.length}; authoredSupport=${supportCandidates.length})`
1997
+ );
1998
+ }
1696
1999
  const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
1697
2000
  const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
1698
2001
  const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
@@ -1706,6 +2009,7 @@ var init_prescribed = __esm({
1706
2009
  surfaceableTargets,
1707
2010
  targetTags,
1708
2011
  supportCandidates,
2012
+ discoveredSupportCandidates,
1709
2013
  supportTags: [...supportTags],
1710
2014
  pressureMultiplier,
1711
2015
  supportMultiplier,
@@ -1776,6 +2080,33 @@ var init_prescribed = __esm({
1776
2080
  }
1777
2081
  return cards;
1778
2082
  }
2083
+ buildDiscoveredSupportCards(runtime, courseId, emittedIds) {
2084
+ if (runtime.blockedTargets.length === 0 || runtime.discoveredSupportCandidates.length === 0) {
2085
+ return [];
2086
+ }
2087
+ const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
2088
+ const supportIds = runtime.discoveredSupportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
2089
+ const cards = [];
2090
+ for (const cardId of supportIds) {
2091
+ emittedIds.add(cardId);
2092
+ cards.push({
2093
+ cardId,
2094
+ courseId,
2095
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
2096
+ provenance: [
2097
+ {
2098
+ strategy: "prescribed",
2099
+ strategyName: this.strategyName || this.name,
2100
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2101
+ action: "generated",
2102
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
2103
+ 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}`
2104
+ }
2105
+ ]
2106
+ });
2107
+ }
2108
+ return cards;
2109
+ }
1779
2110
  findSupportCardsByTags(group, tagsByCard, supportTags) {
1780
2111
  if (supportTags.length === 0) {
1781
2112
  return [];
@@ -1798,6 +2129,40 @@ var init_prescribed = __esm({
1798
2129
  }
1799
2130
  return [...candidates];
1800
2131
  }
2132
+ findDiscoveredSupportCards(args) {
2133
+ const { supportTags, cardsByTag, activeIds, seenIds, excludedIds, limit } = args;
2134
+ const byCardId = /* @__PURE__ */ new Map();
2135
+ for (const supportTag of supportTags) {
2136
+ const taggedCards = cardsByTag.get(supportTag) ?? [];
2137
+ for (const cardId of taggedCards) {
2138
+ if (activeIds.has(cardId) || seenIds.has(cardId) || excludedIds.has(cardId)) {
2139
+ continue;
2140
+ }
2141
+ const existing = byCardId.get(cardId);
2142
+ if (existing) {
2143
+ existing.matches += 1;
2144
+ } else {
2145
+ byCardId.set(cardId, { cardId, matches: 1 });
2146
+ }
2147
+ }
2148
+ }
2149
+ const candidates = [...byCardId.values()].sort((a, b) => b.matches - a.matches || a.cardId.localeCompare(b.cardId));
2150
+ const usedStems = /* @__PURE__ */ new Set();
2151
+ const diverse = [];
2152
+ const deferred = [];
2153
+ for (const entry of candidates) {
2154
+ const stem = extractWordStem(entry.cardId);
2155
+ if (!usedStems.has(stem)) {
2156
+ usedStems.add(stem);
2157
+ diverse.push(entry);
2158
+ } else {
2159
+ deferred.push(entry);
2160
+ }
2161
+ }
2162
+ shuffleInPlace(diverse);
2163
+ shuffleInPlace(deferred);
2164
+ return [...diverse, ...deferred].slice(0, limit).map((entry) => entry.cardId);
2165
+ }
1801
2166
  resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
1802
2167
  const supportTags = /* @__PURE__ */ new Set();
1803
2168
  let blocked = false;
@@ -1843,7 +2208,6 @@ var init_prescribed = __esm({
1843
2208
  }
1844
2209
  collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
1845
2210
  if (depth < 0 || visited.has(tag)) return;
1846
- if (this.isHardGatedTag(tag)) return;
1847
2211
  visited.add(tag);
1848
2212
  let walkedFurther = false;
1849
2213
  for (const hierarchy of hierarchyConfigs) {
@@ -1871,9 +2235,6 @@ var init_prescribed = __esm({
1871
2235
  out.add(tag);
1872
2236
  }
1873
2237
  }
1874
- isHardGatedTag(tag) {
1875
- return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
1876
- }
1877
2238
  isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
1878
2239
  if (!userTagElo) return false;
1879
2240
  const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
@@ -3419,6 +3780,32 @@ var init_Pipeline = __esm({
3419
3780
  cards = await this.hydrateTags(cards);
3420
3781
  const tHydrate = performance.now();
3421
3782
  const allCardsBeforeFiltering = [...cards];
3783
+ const pendingHints = this._ephemeralHints;
3784
+ if (pendingHints?.requireCards?.length) {
3785
+ const poolIds = new Set(allCardsBeforeFiltering.map((c) => c.cardId));
3786
+ const missingIds = pendingHints.requireCards.filter(
3787
+ (p) => !p.includes("*") && !poolIds.has(p)
3788
+ );
3789
+ if (missingIds.length > 0) {
3790
+ const fetchedTags = await this.course.getAppliedTagsBatch(missingIds);
3791
+ const courseId = this.course.getCourseID();
3792
+ for (const cardId of missingIds) {
3793
+ allCardsBeforeFiltering.push({
3794
+ cardId,
3795
+ courseId,
3796
+ score: 1,
3797
+ tags: fetchedTags.get(cardId) ?? [],
3798
+ provenance: []
3799
+ });
3800
+ }
3801
+ logger.info(
3802
+ `[Pipeline] Pre-fetched ${missingIds.length} required card(s) into pool: ${missingIds.join(", ")}`
3803
+ );
3804
+ }
3805
+ }
3806
+ const prescribedIds = new Set(
3807
+ cards.filter((c) => c.provenance.some((p) => p.strategy === "prescribed")).map((c) => c.cardId)
3808
+ );
3422
3809
  const filterImpacts = [];
3423
3810
  for (const filter of this.filters) {
3424
3811
  const beforeCount = cards.length;
@@ -3433,6 +3820,17 @@ var init_Pipeline = __esm({
3433
3820
  else passed++;
3434
3821
  }
3435
3822
  filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
3823
+ if (prescribedIds.size > 0) {
3824
+ const survivingIds = new Set(cards.map((c) => c.cardId));
3825
+ const killedPrescribed = [...prescribedIds].filter((id) => !survivingIds.has(id));
3826
+ const zeroedPrescribed = cards.filter((c) => prescribedIds.has(c.cardId) && c.score === 0).map((c) => c.cardId);
3827
+ if (killedPrescribed.length > 0 || zeroedPrescribed.length > 0) {
3828
+ logger.info(
3829
+ `[Pipeline] Filter '${filter.name}' impact on prescribed cards: ` + (killedPrescribed.length > 0 ? `removed=[${killedPrescribed.join(", ")}] ` : "") + (zeroedPrescribed.length > 0 ? `zeroed=[${zeroedPrescribed.join(", ")}]` : "")
3830
+ );
3831
+ killedPrescribed.forEach((id) => prescribedIds.delete(id));
3832
+ }
3833
+ }
3436
3834
  logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
3437
3835
  }
3438
3836
  cards = cards.filter((c) => c.score > 0);
@@ -3469,7 +3867,8 @@ var init_Pipeline = __esm({
3469
3867
  filterImpacts,
3470
3868
  cards,
3471
3869
  result,
3472
- context.userElo
3870
+ context.userElo,
3871
+ hints ?? void 0
3473
3872
  );
3474
3873
  captureRun(report);
3475
3874
  } catch (e) {
@@ -3579,13 +3978,27 @@ var init_Pipeline = __esm({
3579
3978
  }
3580
3979
  }
3581
3980
  const cardIds = new Set(cards.map((c) => c.cardId));
3981
+ const cardMap = new Map(cards.map((c) => [c.cardId, c]));
3582
3982
  const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
3583
- const inject = (card, reason) => {
3584
- if (!cardIds.has(card.cardId)) {
3585
- const floorScore = Math.max(card.score, 1);
3983
+ const applyRequirement = (card, reason) => {
3984
+ const mandatoryScore = Number.POSITIVE_INFINITY;
3985
+ const existing = cardMap.get(card.cardId);
3986
+ if (existing) {
3987
+ if (existing.score < mandatoryScore) {
3988
+ existing.score = mandatoryScore;
3989
+ existing.provenance.push({
3990
+ strategy: "ephemeralHint",
3991
+ strategyId: "ephemeral-hint",
3992
+ strategyName: hintLabel,
3993
+ action: "boosted",
3994
+ score: mandatoryScore,
3995
+ reason: `${reason} (upgrade to mandatory score)`
3996
+ });
3997
+ }
3998
+ } else {
3586
3999
  cards.push({
3587
4000
  ...card,
3588
- score: floorScore,
4001
+ score: mandatoryScore,
3589
4002
  provenance: [
3590
4003
  ...card.provenance,
3591
4004
  {
@@ -3593,25 +4006,41 @@ var init_Pipeline = __esm({
3593
4006
  strategyId: "ephemeral-hint",
3594
4007
  strategyName: hintLabel,
3595
4008
  action: "boosted",
3596
- score: floorScore,
4009
+ score: mandatoryScore,
3597
4010
  reason
3598
4011
  }
3599
4012
  ]
3600
4013
  });
3601
4014
  cardIds.add(card.cardId);
4015
+ cardMap.set(card.cardId, cards[cards.length - 1]);
3602
4016
  }
3603
4017
  };
3604
4018
  if (hints.requireCards?.length) {
3605
4019
  for (const pattern of hints.requireCards) {
4020
+ for (const cardId of cardIds) {
4021
+ if (globMatch(cardId, pattern)) {
4022
+ applyRequirement(cardMap.get(cardId), `requireCard ${pattern}`);
4023
+ }
4024
+ }
3606
4025
  for (const card of allCards) {
3607
- if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
4026
+ if (globMatch(card.cardId, pattern)) {
4027
+ applyRequirement(card, `requireCard ${pattern}`);
4028
+ }
3608
4029
  }
3609
4030
  }
3610
4031
  }
3611
4032
  if (hints.requireTags?.length) {
3612
4033
  for (const pattern of hints.requireTags) {
4034
+ for (const cardId of cardIds) {
4035
+ const card = cardMap.get(cardId);
4036
+ if (cardMatchesTagPattern(card, pattern)) {
4037
+ applyRequirement(card, `requireTag ${pattern}`);
4038
+ }
4039
+ }
3613
4040
  for (const card of allCards) {
3614
- if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
4041
+ if (cardMatchesTagPattern(card, pattern)) {
4042
+ applyRequirement(card, `requireTag ${pattern}`);
4043
+ }
3615
4044
  }
3616
4045
  }
3617
4046
  }