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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -712,8 +712,9 @@ function getOrigin(card) {
712
712
  const firstEntry = card.provenance[0];
713
713
  if (!firstEntry) return "unknown";
714
714
  const reason = firstEntry.reason?.toLowerCase() || "";
715
- if (reason.includes("new card")) return "new";
716
- if (reason.includes("review")) return "review";
715
+ const strategy = firstEntry.strategy?.toLowerCase() || "";
716
+ if (reason.includes("new card") || strategy.includes("elo")) return "new";
717
+ if (reason.includes("review") || strategy.includes("srs")) return "review";
717
718
  return "unknown";
718
719
  }
719
720
  function captureRun(report) {
@@ -733,12 +734,13 @@ function parseCardElo(provenance) {
733
734
  const match = eloEntry.reason.match(/card:\s*(\d+)/);
734
735
  return match ? parseInt(match[1], 10) : void 0;
735
736
  }
736
- function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
737
+ function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
737
738
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
738
739
  const cards = allCards.map((card) => ({
739
740
  cardId: card.cardId,
740
741
  courseId: card.courseId,
741
742
  origin: getOrigin(card),
743
+ generator: card.provenance[0]?.strategyName || card.provenance[0]?.strategy,
742
744
  finalScore: card.score,
743
745
  cardElo: parseCardElo(card.provenance),
744
746
  provenance: card.provenance,
@@ -755,6 +757,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
755
757
  generators,
756
758
  generatedCount,
757
759
  filters,
760
+ hints,
758
761
  finalCount: selectedCards.length,
759
762
  reviewsSelected,
760
763
  newSelected,
@@ -794,13 +797,169 @@ function printRunSummary(run) {
794
797
  );
795
798
  console.groupEnd();
796
799
  }
800
+ function renderUI() {
801
+ if (!_uiContainer) return;
802
+ const runs = runHistory;
803
+ const selectedRun = _selectedRunIndex !== null ? runs[_selectedRunIndex] : null;
804
+ const styles = `
805
+ #sk-pipeline-debugger {
806
+ position: fixed;
807
+ top: 0;
808
+ left: 0;
809
+ width: 100vw;
810
+ height: 100vh;
811
+ background: #f8f9fa;
812
+ color: #212529;
813
+ z-index: 999999;
814
+ display: flex;
815
+ flex-direction: column;
816
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
817
+ font-size: 14px;
818
+ }
819
+ #sk-pipeline-debugger header {
820
+ padding: 1rem;
821
+ background: #343a40;
822
+ color: white;
823
+ display: flex;
824
+ justify-content: space-between;
825
+ align-items: center;
826
+ }
827
+ #sk-pipeline-debugger .container {
828
+ display: flex;
829
+ flex: 1;
830
+ overflow: hidden;
831
+ }
832
+ #sk-pipeline-debugger .sidebar {
833
+ width: 300px;
834
+ border-right: 1px solid #dee2e6;
835
+ overflow-y: auto;
836
+ background: white;
837
+ }
838
+ #sk-pipeline-debugger .main-content {
839
+ flex: 1;
840
+ overflow-y: auto;
841
+ padding: 1.5rem;
842
+ }
843
+ #sk-pipeline-debugger .run-item {
844
+ padding: 0.75rem 1rem;
845
+ border-bottom: 1px solid #eee;
846
+ cursor: pointer;
847
+ }
848
+ #sk-pipeline-debugger .run-item:hover { background: #f1f3f5; }
849
+ #sk-pipeline-debugger .run-item.active { background: #e9ecef; border-left: 4px solid #007bff; }
850
+ #sk-pipeline-debugger h2, #sk-pipeline-debugger h3 { margin-top: 0; }
851
+ #sk-pipeline-debugger table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; background: white; }
852
+ #sk-pipeline-debugger th, #sk-pipeline-debugger td { border: 1px solid #dee2e6; padding: 0.5rem; text-align: left; }
853
+ #sk-pipeline-debugger th { background: #f1f3f5; }
854
+ #sk-pipeline-debugger code { background: #f1f3f5; padding: 0.1rem 0.3rem; border-radius: 3px; font-family: monospace; }
855
+ #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
856
+ #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
857
+ #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; }
858
+ `;
859
+ const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
860
+ (r, i) => `
861
+ <div class="run-item ${i === _selectedRunIndex ? "active" : ""}" onclick="window.skuilder.pipeline._selectRun(${i})">
862
+ <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
863
+ <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
864
+ <small>${r.finalCount} cards selected</small>
865
+ </div>
866
+ `
867
+ ).join("");
868
+ let detailsHtml = '<div style="color: #6c757d; text-align: center; margin-top: 5rem;">Select a run to see details</div>';
869
+ if (selectedRun) {
870
+ const filteredCards = selectedRun.cards.filter(
871
+ (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
872
+ );
873
+ detailsHtml = `
874
+ <h2>Run: ${selectedRun.runId}</h2>
875
+ <p>
876
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
877
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
878
+ <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
879
+ </p>
880
+
881
+ <h3>Pipeline Config</h3>
882
+ <table>
883
+ <tr><th>Generator</th><td>${selectedRun.generatorName} (${selectedRun.generatedCount} candidates)</td></tr>
884
+ ${(selectedRun.generators || []).map(
885
+ (g) => `
886
+ <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>
887
+ `
888
+ ).join("")}
889
+ </table>
890
+
891
+ ${selectedRun.hints ? `
892
+ <h3>Ephemeral Hints</h3>
893
+ <table>
894
+ ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
895
+ ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
896
+ ${selectedRun.hints.boostCards ? `<tr><th>Boost Cards</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostCards, null, 2)}</pre></td></tr>` : ""}
897
+ ${selectedRun.hints.requireTags ? `<tr><th>Require Tags</th><td>${selectedRun.hints.requireTags.join(", ")}</td></tr>` : ""}
898
+ ${selectedRun.hints.requireCards ? `<tr><th>Require Cards</th><td>${selectedRun.hints.requireCards.join(", ")}</td></tr>` : ""}
899
+ ${selectedRun.hints.excludeTags ? `<tr><th>Exclude Tags</th><td>${selectedRun.hints.excludeTags.join(", ")}</td></tr>` : ""}
900
+ ${selectedRun.hints.excludeCards ? `<tr><th>Exclude Cards</th><td>${selectedRun.hints.excludeCards.join(", ")}</td></tr>` : ""}
901
+ </table>
902
+ ` : ""}
903
+
904
+ <h3>Filter Impact</h3>
905
+ <table>
906
+ <thead><tr><th>Filter</th><th>Boosted</th><th>Penalized</th><th>Passed</th><th>Removed</th></tr></thead>
907
+ <tbody>
908
+ ${selectedRun.filters.map(
909
+ (f) => `
910
+ <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>
911
+ `
912
+ ).join("")}
913
+ </tbody>
914
+ </table>
915
+
916
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
917
+ <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
918
+
919
+ <table>
920
+ <thead><tr><th>ID</th><th>Generator</th><th>Origin</th><th>Score</th><th>Selected</th></tr></thead>
921
+ <tbody>
922
+ ${filteredCards.map(
923
+ (c) => `
924
+ <tr>
925
+ <td><code>${c.cardId}</code></td>
926
+ <td>${c.generator || "unknown"}</td>
927
+ <td>${c.origin}</td>
928
+ <td>${c.finalScore.toFixed(3)}</td>
929
+ <td>${c.selected ? "\u2705" : "\u274C"}</td>
930
+ </tr>
931
+ ${c.selected || _cardSearchQuery ? `
932
+ <tr>
933
+ <td colspan="5">
934
+ <div class="provenance">${formatProvenance(c.provenance)}</div>
935
+ </td>
936
+ </tr>
937
+ ` : ""}
938
+ `
939
+ ).join("")}
940
+ </tbody>
941
+ </table>
942
+ `;
943
+ }
944
+ _uiContainer.innerHTML = `
945
+ <style>${styles}</style>
946
+ <header>
947
+ <strong>Pipeline Debugger</strong>
948
+ <button class="close-btn" onclick="window.skuilder.pipeline.ui()">Close</button>
949
+ </header>
950
+ <div class="container">
951
+ <div class="sidebar">${runListHtml}</div>
952
+ <div class="main-content">${detailsHtml}</div>
953
+ </div>
954
+ `;
955
+ }
797
956
  function mountPipelineDebugger() {
798
957
  if (typeof window === "undefined") return;
799
958
  const win = window;
800
959
  win.skuilder = win.skuilder || {};
801
960
  win.skuilder.pipeline = pipelineDebugAPI;
802
961
  }
803
- var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
962
+ var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
804
963
  var init_PipelineDebugger = __esm({
805
964
  "src/core/navigators/PipelineDebugger.ts"() {
806
965
  "use strict";
@@ -809,6 +968,9 @@ var init_PipelineDebugger = __esm({
809
968
  _activePipeline = null;
810
969
  MAX_RUNS = 10;
811
970
  runHistory = [];
971
+ _uiContainer = null;
972
+ _selectedRunIndex = null;
973
+ _cardSearchQuery = "";
812
974
  pipelineDebugAPI = {
813
975
  /**
814
976
  * Get raw run history for programmatic access.
@@ -948,16 +1110,20 @@ var init_PipelineDebugger = __esm({
948
1110
  const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
949
1111
  const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
950
1112
  const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
1113
+ const supportCard = reason.match(/supportCard=([^;]+)/)?.[1] ?? "none";
951
1114
  const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
952
1115
  const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
1116
+ const supportSource = mode === "discovered-support" ? "discovered" : mode === "support" ? "authored" : "n/a";
953
1117
  return {
954
1118
  group: parsedGroup,
955
1119
  mode,
1120
+ supportSource,
956
1121
  cardId: card.cardId,
957
1122
  selected: card.selected ? "yes" : "no",
958
1123
  finalScore: card.finalScore.toFixed(3),
959
1124
  blocked,
960
1125
  blockedTargets,
1126
+ supportCard,
961
1127
  supportTags,
962
1128
  multiplier
963
1129
  };
@@ -973,6 +1139,8 @@ var init_PipelineDebugger = __esm({
973
1139
  const selectedRows = rows.filter((r) => r.selected === "yes");
974
1140
  const blockedTargetSet = /* @__PURE__ */ new Set();
975
1141
  const supportTagSet = /* @__PURE__ */ new Set();
1142
+ const authoredSupportSet = /* @__PURE__ */ new Set();
1143
+ const discoveredSupportSet = /* @__PURE__ */ new Set();
976
1144
  for (const row of rows) {
977
1145
  if (row.blockedTargets && row.blockedTargets !== "none") {
978
1146
  row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
@@ -980,6 +1148,13 @@ var init_PipelineDebugger = __esm({
980
1148
  if (row.supportTags && row.supportTags !== "none") {
981
1149
  row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
982
1150
  }
1151
+ if (row.supportCard && row.supportCard !== "none") {
1152
+ if (row.supportSource === "discovered") {
1153
+ discoveredSupportSet.add(row.supportCard);
1154
+ } else if (row.supportSource === "authored") {
1155
+ authoredSupportSet.add(row.supportCard);
1156
+ }
1157
+ }
983
1158
  }
984
1159
  logger.info(`Prescribed cards in run: ${rows.length}`);
985
1160
  logger.info(`Selected prescribed cards: ${selectedRows.length}`);
@@ -989,6 +1164,12 @@ var init_PipelineDebugger = __esm({
989
1164
  logger.info(
990
1165
  `Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
991
1166
  );
1167
+ logger.info(
1168
+ `Authored support cards emitted: ${authoredSupportSet.size > 0 ? [...authoredSupportSet].join(", ") : "none"}`
1169
+ );
1170
+ logger.info(
1171
+ `Discovered support cards emitted: ${discoveredSupportSet.size > 0 ? [...discoveredSupportSet].join(", ") : "none"}`
1172
+ );
992
1173
  console.groupEnd();
993
1174
  },
994
1175
  /**
@@ -1123,6 +1304,39 @@ var init_PipelineDebugger = __esm({
1123
1304
  Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
1124
1305
  );
1125
1306
  },
1307
+ /**
1308
+ * Toggle the full-screen UI debugger.
1309
+ */
1310
+ ui() {
1311
+ if (_uiContainer) {
1312
+ document.body.removeChild(_uiContainer);
1313
+ _uiContainer = null;
1314
+ return;
1315
+ }
1316
+ _uiContainer = document.createElement("div");
1317
+ _uiContainer.id = "sk-pipeline-debugger";
1318
+ document.body.appendChild(_uiContainer);
1319
+ if (_selectedRunIndex === null && runHistory.length > 0) {
1320
+ _selectedRunIndex = 0;
1321
+ }
1322
+ renderUI();
1323
+ },
1324
+ /**
1325
+ * Internal UI helpers
1326
+ * @internal
1327
+ */
1328
+ _selectRun(index) {
1329
+ _selectedRunIndex = index;
1330
+ renderUI();
1331
+ },
1332
+ /**
1333
+ * Internal UI helpers
1334
+ * @internal
1335
+ */
1336
+ _setSearch(query) {
1337
+ _cardSearchQuery = query;
1338
+ renderUI();
1339
+ },
1126
1340
  /**
1127
1341
  * Show help.
1128
1342
  */
@@ -1131,6 +1345,7 @@ var init_PipelineDebugger = __esm({
1131
1345
  \u{1F527} Pipeline Debug API
1132
1346
 
1133
1347
  Commands:
1348
+ .ui() Toggle full-screen UI debugger
1134
1349
  .showLastRun() Show summary of most recent pipeline run
1135
1350
  .showRun(id|index) Show summary of a specific run (by index or ID suffix)
1136
1351
  .showCard(cardId) Show provenance trail for a specific card
@@ -1147,6 +1362,7 @@ Commands:
1147
1362
  .help() Show this help message
1148
1363
 
1149
1364
  Example:
1365
+ window.skuilder.pipeline.ui()
1150
1366
  window.skuilder.pipeline.showLastRun()
1151
1367
  window.skuilder.pipeline.showRun(1)
1152
1368
  await window.skuilder.pipeline.diagnoseCardSpace()
@@ -1309,7 +1525,7 @@ var init_CompositeGenerator = __esm({
1309
1525
  for (const [, items] of byCardId) {
1310
1526
  const cards2 = items.map((i) => i.card);
1311
1527
  const aggregatedScore = this.aggregateScores(items);
1312
- const finalScore = Math.min(1, aggregatedScore);
1528
+ const finalScore = Math.max(0, aggregatedScore);
1313
1529
  const mergedProvenance = cards2.flatMap((c) => c.provenance);
1314
1530
  const initialScore = cards2[0].score;
1315
1531
  const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
@@ -1506,10 +1722,26 @@ function matchesTagPattern(tag, pattern) {
1506
1722
  const re = new RegExp(`^${escaped}$`);
1507
1723
  return re.test(tag);
1508
1724
  }
1725
+ function extractWordStem(cardId) {
1726
+ for (const prefix of ["c-ml-", "c-ws-", "c-spelling-"]) {
1727
+ if (cardId.startsWith(prefix)) {
1728
+ const rest = cardId.slice(prefix.length);
1729
+ const lastDash = rest.lastIndexOf("-");
1730
+ return lastDash > 0 ? rest.slice(0, lastDash) : rest;
1731
+ }
1732
+ }
1733
+ return cardId;
1734
+ }
1735
+ function shuffleInPlace(arr) {
1736
+ for (let i = arr.length - 1; i > 0; i--) {
1737
+ const j = Math.floor(Math.random() * (i + 1));
1738
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1739
+ }
1740
+ }
1509
1741
  function pickTopByScore(cards, limit) {
1510
1742
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1511
1743
  }
1512
- 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;
1744
+ 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;
1513
1745
  var init_prescribed = __esm({
1514
1746
  "src/core/navigators/generators/prescribed.ts"() {
1515
1747
  "use strict";
@@ -1522,11 +1754,10 @@ var init_prescribed = __esm({
1522
1754
  DEFAULT_MIN_COUNT = 3;
1523
1755
  BASE_TARGET_SCORE = 1;
1524
1756
  BASE_SUPPORT_SCORE = 0.8;
1757
+ DISCOVERED_SUPPORT_SCORE = 12;
1525
1758
  MAX_TARGET_MULTIPLIER = 8;
1526
1759
  MAX_SUPPORT_MULTIPLIER = 4;
1527
- LOCKED_TAG_PREFIXES = ["concept:"];
1528
- LESSON_GATE_PENALTY_TAG_HINT = "concept:";
1529
- PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
1760
+ PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
1530
1761
  PrescribedCardsGenerator = class extends ContentNavigator {
1531
1762
  name;
1532
1763
  config;
@@ -1562,6 +1793,20 @@ var init_prescribed = __esm({
1562
1793
  const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
1563
1794
  const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
1564
1795
  const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
1796
+ const courseTagDocs = await this.course.getCourseTagStubs().catch(
1797
+ () => ({
1798
+ rows: [],
1799
+ offset: 0,
1800
+ total_rows: 0
1801
+ })
1802
+ );
1803
+ const cardsByTag = /* @__PURE__ */ new Map();
1804
+ for (const row of courseTagDocs.rows ?? []) {
1805
+ const tagDoc = row.doc;
1806
+ if (tagDoc?.name && Array.isArray(tagDoc.taggedCards)) {
1807
+ cardsByTag.set(tagDoc.name, [...tagDoc.taggedCards]);
1808
+ }
1809
+ }
1565
1810
  const nextState = {
1566
1811
  updatedAt: isoNow(),
1567
1812
  groups: {}
@@ -1576,11 +1821,31 @@ var init_prescribed = __esm({
1576
1821
  activeIds,
1577
1822
  seenIds,
1578
1823
  tagsByCard,
1824
+ cardsByTag,
1579
1825
  hierarchyConfigs,
1580
1826
  userTagElo,
1581
1827
  userGlobalElo
1582
1828
  });
1583
1829
  groupRuntimes.push(runtime);
1830
+ logger.info(
1831
+ `[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)}`
1832
+ );
1833
+ if (runtime.blockedTargets.length > 0) {
1834
+ logger.info(
1835
+ `[Prescribed] Group '${group.id}' blocked targets: ${runtime.blockedTargets.join(", ")}`
1836
+ );
1837
+ logger.info(
1838
+ `[Prescribed] Group '${group.id}' support tags needed: ${runtime.supportTags.join(", ") || "(none)"}`
1839
+ );
1840
+ logger.info(
1841
+ `[Prescribed] Group '${group.id}' escalation mode: ` + (runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support-candidates" : "boost-only")
1842
+ );
1843
+ if (runtime.discoveredSupportCandidates.length > 0) {
1844
+ logger.info(
1845
+ `[Prescribed] Group '${group.id}' discovered support candidates: ${runtime.discoveredSupportCandidates.join(", ")}`
1846
+ );
1847
+ }
1848
+ }
1584
1849
  nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
1585
1850
  const directCards = this.buildDirectTargetCards(
1586
1851
  runtime,
@@ -1592,15 +1857,30 @@ var init_prescribed = __esm({
1592
1857
  courseId,
1593
1858
  emittedIds
1594
1859
  );
1595
- emitted.push(...directCards, ...supportCards);
1860
+ const discoveredSupportCards = this.buildDiscoveredSupportCards(
1861
+ runtime,
1862
+ courseId,
1863
+ emittedIds
1864
+ );
1865
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
1596
1866
  }
1597
1867
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1598
1868
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
1599
1869
  boostTags: hintSummary.boostTags,
1600
1870
  _label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
1601
1871
  } : void 0;
1872
+ if (hints) {
1873
+ const tagEntries = Object.entries(hints.boostTags ?? {});
1874
+ logger.info(
1875
+ `[Prescribed] Emitting ${tagEntries.length} boost hint(s): ` + tagEntries.map(([tag, mult]) => `${tag}\xD7${mult.toFixed(1)}`).join(", ")
1876
+ );
1877
+ } else {
1878
+ logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
1879
+ }
1602
1880
  if (emitted.length === 0) {
1603
- logger.debug("[Prescribed] No prescribed targets/support emitted this run");
1881
+ logger.info(
1882
+ "[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
1883
+ );
1604
1884
  await this.putStrategyState(nextState).catch((e) => {
1605
1885
  logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
1606
1886
  });
@@ -1633,7 +1913,7 @@ var init_prescribed = __esm({
1633
1913
  logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
1634
1914
  });
1635
1915
  logger.info(
1636
- `[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)`
1916
+ `[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)`
1637
1917
  );
1638
1918
  return hints ? { cards: finalCards, hints } : { cards: finalCards };
1639
1919
  }
@@ -1663,9 +1943,15 @@ var init_prescribed = __esm({
1663
1943
  const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
1664
1944
  const groups = groupsRaw.map((raw, i) => ({
1665
1945
  id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
1666
- targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
1667
- supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
1668
- supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
1946
+ targetCardIds: dedupe(
1947
+ Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []
1948
+ ),
1949
+ supportCardIds: dedupe(
1950
+ Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []
1951
+ ),
1952
+ supportTagPatterns: dedupe(
1953
+ Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []
1954
+ ),
1669
1955
  freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
1670
1956
  maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
1671
1957
  maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
@@ -1682,7 +1968,7 @@ var init_prescribed = __esm({
1682
1968
  }
1683
1969
  async loadHierarchyConfigs() {
1684
1970
  try {
1685
- const strategies = await this.course.getNavigationStrategies();
1971
+ const strategies = await this.course.getAllNavigationStrategies();
1686
1972
  return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
1687
1973
  try {
1688
1974
  const parsed = JSON.parse(s.serializedData);
@@ -1705,6 +1991,7 @@ var init_prescribed = __esm({
1705
1991
  activeIds,
1706
1992
  seenIds,
1707
1993
  tagsByCard,
1994
+ cardsByTag,
1708
1995
  hierarchyConfigs,
1709
1996
  userTagElo,
1710
1997
  userGlobalElo
@@ -1768,6 +2055,22 @@ var init_prescribed = __esm({
1768
2055
  [...supportTags]
1769
2056
  )
1770
2057
  ]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
2058
+ const discoveredSupportCandidates = blockedTargets.length > 0 && supportTags.size > 0 && supportCandidates.length === 0 ? this.findDiscoveredSupportCards({
2059
+ supportTags: [...supportTags],
2060
+ cardsByTag,
2061
+ activeIds,
2062
+ seenIds,
2063
+ excludedIds: /* @__PURE__ */ new Set([
2064
+ ...group.targetCardIds,
2065
+ ...group.supportCardIds ?? []
2066
+ ]),
2067
+ limit: group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN
2068
+ }) : [];
2069
+ if (blockedTargets.length > 0 && supportTags.size > 0 && discoveredSupportCandidates.length === 0) {
2070
+ logger.info(
2071
+ `[Prescribed] Group '${group.id}' discovered 0 broader support candidates (blocked=${blockedTargets.length}; authoredSupport=${supportCandidates.length})`
2072
+ );
2073
+ }
1771
2074
  const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
1772
2075
  const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
1773
2076
  const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
@@ -1781,6 +2084,7 @@ var init_prescribed = __esm({
1781
2084
  surfaceableTargets,
1782
2085
  targetTags,
1783
2086
  supportCandidates,
2087
+ discoveredSupportCandidates,
1784
2088
  supportTags: [...supportTags],
1785
2089
  pressureMultiplier,
1786
2090
  supportMultiplier,
@@ -1851,6 +2155,33 @@ var init_prescribed = __esm({
1851
2155
  }
1852
2156
  return cards;
1853
2157
  }
2158
+ buildDiscoveredSupportCards(runtime, courseId, emittedIds) {
2159
+ if (runtime.blockedTargets.length === 0 || runtime.discoveredSupportCandidates.length === 0) {
2160
+ return [];
2161
+ }
2162
+ const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
2163
+ const supportIds = runtime.discoveredSupportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
2164
+ const cards = [];
2165
+ for (const cardId of supportIds) {
2166
+ emittedIds.add(cardId);
2167
+ cards.push({
2168
+ cardId,
2169
+ courseId,
2170
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
2171
+ provenance: [
2172
+ {
2173
+ strategy: "prescribed",
2174
+ strategyName: this.strategyName || this.name,
2175
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2176
+ action: "generated",
2177
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
2178
+ 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}`
2179
+ }
2180
+ ]
2181
+ });
2182
+ }
2183
+ return cards;
2184
+ }
1854
2185
  findSupportCardsByTags(group, tagsByCard, supportTags) {
1855
2186
  if (supportTags.length === 0) {
1856
2187
  return [];
@@ -1873,6 +2204,40 @@ var init_prescribed = __esm({
1873
2204
  }
1874
2205
  return [...candidates];
1875
2206
  }
2207
+ findDiscoveredSupportCards(args) {
2208
+ const { supportTags, cardsByTag, activeIds, seenIds, excludedIds, limit } = args;
2209
+ const byCardId = /* @__PURE__ */ new Map();
2210
+ for (const supportTag of supportTags) {
2211
+ const taggedCards = cardsByTag.get(supportTag) ?? [];
2212
+ for (const cardId of taggedCards) {
2213
+ if (activeIds.has(cardId) || seenIds.has(cardId) || excludedIds.has(cardId)) {
2214
+ continue;
2215
+ }
2216
+ const existing = byCardId.get(cardId);
2217
+ if (existing) {
2218
+ existing.matches += 1;
2219
+ } else {
2220
+ byCardId.set(cardId, { cardId, matches: 1 });
2221
+ }
2222
+ }
2223
+ }
2224
+ const candidates = [...byCardId.values()].sort((a, b) => b.matches - a.matches || a.cardId.localeCompare(b.cardId));
2225
+ const usedStems = /* @__PURE__ */ new Set();
2226
+ const diverse = [];
2227
+ const deferred = [];
2228
+ for (const entry of candidates) {
2229
+ const stem = extractWordStem(entry.cardId);
2230
+ if (!usedStems.has(stem)) {
2231
+ usedStems.add(stem);
2232
+ diverse.push(entry);
2233
+ } else {
2234
+ deferred.push(entry);
2235
+ }
2236
+ }
2237
+ shuffleInPlace(diverse);
2238
+ shuffleInPlace(deferred);
2239
+ return [...diverse, ...deferred].slice(0, limit).map((entry) => entry.cardId);
2240
+ }
1876
2241
  resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
1877
2242
  const supportTags = /* @__PURE__ */ new Set();
1878
2243
  let blocked = false;
@@ -1918,7 +2283,6 @@ var init_prescribed = __esm({
1918
2283
  }
1919
2284
  collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
1920
2285
  if (depth < 0 || visited.has(tag)) return;
1921
- if (this.isHardGatedTag(tag)) return;
1922
2286
  visited.add(tag);
1923
2287
  let walkedFurther = false;
1924
2288
  for (const hierarchy of hierarchyConfigs) {
@@ -1946,9 +2310,6 @@ var init_prescribed = __esm({
1946
2310
  out.add(tag);
1947
2311
  }
1948
2312
  }
1949
- isHardGatedTag(tag) {
1950
- return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
1951
- }
1952
2313
  isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
1953
2314
  if (!userTagElo) return false;
1954
2315
  const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
@@ -3740,6 +4101,32 @@ var init_Pipeline = __esm({
3740
4101
  cards = await this.hydrateTags(cards);
3741
4102
  const tHydrate = performance.now();
3742
4103
  const allCardsBeforeFiltering = [...cards];
4104
+ const pendingHints = this._ephemeralHints;
4105
+ if (pendingHints?.requireCards?.length) {
4106
+ const poolIds = new Set(allCardsBeforeFiltering.map((c) => c.cardId));
4107
+ const missingIds = pendingHints.requireCards.filter(
4108
+ (p) => !p.includes("*") && !poolIds.has(p)
4109
+ );
4110
+ if (missingIds.length > 0) {
4111
+ const fetchedTags = await this.course.getAppliedTagsBatch(missingIds);
4112
+ const courseId = this.course.getCourseID();
4113
+ for (const cardId of missingIds) {
4114
+ allCardsBeforeFiltering.push({
4115
+ cardId,
4116
+ courseId,
4117
+ score: 1,
4118
+ tags: fetchedTags.get(cardId) ?? [],
4119
+ provenance: []
4120
+ });
4121
+ }
4122
+ logger.info(
4123
+ `[Pipeline] Pre-fetched ${missingIds.length} required card(s) into pool: ${missingIds.join(", ")}`
4124
+ );
4125
+ }
4126
+ }
4127
+ const prescribedIds = new Set(
4128
+ cards.filter((c) => c.provenance.some((p) => p.strategy === "prescribed")).map((c) => c.cardId)
4129
+ );
3743
4130
  const filterImpacts = [];
3744
4131
  for (const filter of this.filters) {
3745
4132
  const beforeCount = cards.length;
@@ -3754,6 +4141,17 @@ var init_Pipeline = __esm({
3754
4141
  else passed++;
3755
4142
  }
3756
4143
  filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
4144
+ if (prescribedIds.size > 0) {
4145
+ const survivingIds = new Set(cards.map((c) => c.cardId));
4146
+ const killedPrescribed = [...prescribedIds].filter((id) => !survivingIds.has(id));
4147
+ const zeroedPrescribed = cards.filter((c) => prescribedIds.has(c.cardId) && c.score === 0).map((c) => c.cardId);
4148
+ if (killedPrescribed.length > 0 || zeroedPrescribed.length > 0) {
4149
+ logger.info(
4150
+ `[Pipeline] Filter '${filter.name}' impact on prescribed cards: ` + (killedPrescribed.length > 0 ? `removed=[${killedPrescribed.join(", ")}] ` : "") + (zeroedPrescribed.length > 0 ? `zeroed=[${zeroedPrescribed.join(", ")}]` : "")
4151
+ );
4152
+ killedPrescribed.forEach((id) => prescribedIds.delete(id));
4153
+ }
4154
+ }
3757
4155
  logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
3758
4156
  }
3759
4157
  cards = cards.filter((c) => c.score > 0);
@@ -3790,7 +4188,8 @@ var init_Pipeline = __esm({
3790
4188
  filterImpacts,
3791
4189
  cards,
3792
4190
  result,
3793
- context.userElo
4191
+ context.userElo,
4192
+ hints ?? void 0
3794
4193
  );
3795
4194
  captureRun(report);
3796
4195
  } catch (e) {
@@ -3900,13 +4299,27 @@ var init_Pipeline = __esm({
3900
4299
  }
3901
4300
  }
3902
4301
  const cardIds = new Set(cards.map((c) => c.cardId));
4302
+ const cardMap = new Map(cards.map((c) => [c.cardId, c]));
3903
4303
  const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
3904
- const inject = (card, reason) => {
3905
- if (!cardIds.has(card.cardId)) {
3906
- const floorScore = Math.max(card.score, 1);
4304
+ const applyRequirement = (card, reason) => {
4305
+ const mandatoryScore = Number.POSITIVE_INFINITY;
4306
+ const existing = cardMap.get(card.cardId);
4307
+ if (existing) {
4308
+ if (existing.score < mandatoryScore) {
4309
+ existing.score = mandatoryScore;
4310
+ existing.provenance.push({
4311
+ strategy: "ephemeralHint",
4312
+ strategyId: "ephemeral-hint",
4313
+ strategyName: hintLabel,
4314
+ action: "boosted",
4315
+ score: mandatoryScore,
4316
+ reason: `${reason} (upgrade to mandatory score)`
4317
+ });
4318
+ }
4319
+ } else {
3907
4320
  cards.push({
3908
4321
  ...card,
3909
- score: floorScore,
4322
+ score: mandatoryScore,
3910
4323
  provenance: [
3911
4324
  ...card.provenance,
3912
4325
  {
@@ -3914,25 +4327,41 @@ var init_Pipeline = __esm({
3914
4327
  strategyId: "ephemeral-hint",
3915
4328
  strategyName: hintLabel,
3916
4329
  action: "boosted",
3917
- score: floorScore,
4330
+ score: mandatoryScore,
3918
4331
  reason
3919
4332
  }
3920
4333
  ]
3921
4334
  });
3922
4335
  cardIds.add(card.cardId);
4336
+ cardMap.set(card.cardId, cards[cards.length - 1]);
3923
4337
  }
3924
4338
  };
3925
4339
  if (hints.requireCards?.length) {
3926
4340
  for (const pattern of hints.requireCards) {
4341
+ for (const cardId of cardIds) {
4342
+ if (globMatch(cardId, pattern)) {
4343
+ applyRequirement(cardMap.get(cardId), `requireCard ${pattern}`);
4344
+ }
4345
+ }
3927
4346
  for (const card of allCards) {
3928
- if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
4347
+ if (globMatch(card.cardId, pattern)) {
4348
+ applyRequirement(card, `requireCard ${pattern}`);
4349
+ }
3929
4350
  }
3930
4351
  }
3931
4352
  }
3932
4353
  if (hints.requireTags?.length) {
3933
4354
  for (const pattern of hints.requireTags) {
4355
+ for (const cardId of cardIds) {
4356
+ const card = cardMap.get(cardId);
4357
+ if (cardMatchesTagPattern(card, pattern)) {
4358
+ applyRequirement(card, `requireTag ${pattern}`);
4359
+ }
4360
+ }
3934
4361
  for (const card of allCards) {
3935
- if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
4362
+ if (cardMatchesTagPattern(card, pattern)) {
4363
+ applyRequirement(card, `requireTag ${pattern}`);
4364
+ }
3936
4365
  }
3937
4366
  }
3938
4367
  }
@@ -5046,7 +5475,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
5046
5475
  }
5047
5476
  }
5048
5477
  async getCourseDoc(id, options) {
5049
- return await this.db.get(id, options);
5478
+ return await this.db.get(id, options ?? {});
5050
5479
  }
5051
5480
  async getCourseDocs(ids, options = {}) {
5052
5481
  return await this.db.allDocs({
@@ -5130,7 +5559,9 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
5130
5559
  );
5131
5560
  return pipeline;
5132
5561
  } catch (e) {
5133
- logger.error(`[courseDB] Error creating navigator: ${e}`);
5562
+ const msg = e instanceof Error ? `${e.message}
5563
+ ${e.stack}` : JSON.stringify(e);
5564
+ logger.error(`[courseDB] Error creating navigator: ${msg}`);
5134
5565
  throw e;
5135
5566
  }
5136
5567
  }