@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.
@@ -735,8 +735,9 @@ function getOrigin(card) {
735
735
  const firstEntry = card.provenance[0];
736
736
  if (!firstEntry) return "unknown";
737
737
  const reason = firstEntry.reason?.toLowerCase() || "";
738
- if (reason.includes("new card")) return "new";
739
- if (reason.includes("review")) return "review";
738
+ const strategy = firstEntry.strategy?.toLowerCase() || "";
739
+ if (reason.includes("new card") || strategy.includes("elo")) return "new";
740
+ if (reason.includes("review") || strategy.includes("srs")) return "review";
740
741
  return "unknown";
741
742
  }
742
743
  function captureRun(report) {
@@ -756,12 +757,13 @@ function parseCardElo(provenance) {
756
757
  const match = eloEntry.reason.match(/card:\s*(\d+)/);
757
758
  return match ? parseInt(match[1], 10) : void 0;
758
759
  }
759
- function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
760
+ function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
760
761
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
761
762
  const cards = allCards.map((card) => ({
762
763
  cardId: card.cardId,
763
764
  courseId: card.courseId,
764
765
  origin: getOrigin(card),
766
+ generator: card.provenance[0]?.strategyName || card.provenance[0]?.strategy,
765
767
  finalScore: card.score,
766
768
  cardElo: parseCardElo(card.provenance),
767
769
  provenance: card.provenance,
@@ -778,6 +780,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
778
780
  generators,
779
781
  generatedCount,
780
782
  filters,
783
+ hints,
781
784
  finalCount: selectedCards.length,
782
785
  reviewsSelected,
783
786
  newSelected,
@@ -817,13 +820,169 @@ function printRunSummary(run) {
817
820
  );
818
821
  console.groupEnd();
819
822
  }
823
+ function renderUI() {
824
+ if (!_uiContainer) return;
825
+ const runs = runHistory;
826
+ const selectedRun = _selectedRunIndex !== null ? runs[_selectedRunIndex] : null;
827
+ const styles = `
828
+ #sk-pipeline-debugger {
829
+ position: fixed;
830
+ top: 0;
831
+ left: 0;
832
+ width: 100vw;
833
+ height: 100vh;
834
+ background: #f8f9fa;
835
+ color: #212529;
836
+ z-index: 999999;
837
+ display: flex;
838
+ flex-direction: column;
839
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
840
+ font-size: 14px;
841
+ }
842
+ #sk-pipeline-debugger header {
843
+ padding: 1rem;
844
+ background: #343a40;
845
+ color: white;
846
+ display: flex;
847
+ justify-content: space-between;
848
+ align-items: center;
849
+ }
850
+ #sk-pipeline-debugger .container {
851
+ display: flex;
852
+ flex: 1;
853
+ overflow: hidden;
854
+ }
855
+ #sk-pipeline-debugger .sidebar {
856
+ width: 300px;
857
+ border-right: 1px solid #dee2e6;
858
+ overflow-y: auto;
859
+ background: white;
860
+ }
861
+ #sk-pipeline-debugger .main-content {
862
+ flex: 1;
863
+ overflow-y: auto;
864
+ padding: 1.5rem;
865
+ }
866
+ #sk-pipeline-debugger .run-item {
867
+ padding: 0.75rem 1rem;
868
+ border-bottom: 1px solid #eee;
869
+ cursor: pointer;
870
+ }
871
+ #sk-pipeline-debugger .run-item:hover { background: #f1f3f5; }
872
+ #sk-pipeline-debugger .run-item.active { background: #e9ecef; border-left: 4px solid #007bff; }
873
+ #sk-pipeline-debugger h2, #sk-pipeline-debugger h3 { margin-top: 0; }
874
+ #sk-pipeline-debugger table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; background: white; }
875
+ #sk-pipeline-debugger th, #sk-pipeline-debugger td { border: 1px solid #dee2e6; padding: 0.5rem; text-align: left; }
876
+ #sk-pipeline-debugger th { background: #f1f3f5; }
877
+ #sk-pipeline-debugger code { background: #f1f3f5; padding: 0.1rem 0.3rem; border-radius: 3px; font-family: monospace; }
878
+ #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
879
+ #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
880
+ #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; }
881
+ `;
882
+ const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
883
+ (r, i) => `
884
+ <div class="run-item ${i === _selectedRunIndex ? "active" : ""}" onclick="window.skuilder.pipeline._selectRun(${i})">
885
+ <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
886
+ <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
887
+ <small>${r.finalCount} cards selected</small>
888
+ </div>
889
+ `
890
+ ).join("");
891
+ let detailsHtml = '<div style="color: #6c757d; text-align: center; margin-top: 5rem;">Select a run to see details</div>';
892
+ if (selectedRun) {
893
+ const filteredCards = selectedRun.cards.filter(
894
+ (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
895
+ );
896
+ detailsHtml = `
897
+ <h2>Run: ${selectedRun.runId}</h2>
898
+ <p>
899
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
900
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
901
+ <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
902
+ </p>
903
+
904
+ <h3>Pipeline Config</h3>
905
+ <table>
906
+ <tr><th>Generator</th><td>${selectedRun.generatorName} (${selectedRun.generatedCount} candidates)</td></tr>
907
+ ${(selectedRun.generators || []).map(
908
+ (g) => `
909
+ <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>
910
+ `
911
+ ).join("")}
912
+ </table>
913
+
914
+ ${selectedRun.hints ? `
915
+ <h3>Ephemeral Hints</h3>
916
+ <table>
917
+ ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
918
+ ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
919
+ ${selectedRun.hints.boostCards ? `<tr><th>Boost Cards</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostCards, null, 2)}</pre></td></tr>` : ""}
920
+ ${selectedRun.hints.requireTags ? `<tr><th>Require Tags</th><td>${selectedRun.hints.requireTags.join(", ")}</td></tr>` : ""}
921
+ ${selectedRun.hints.requireCards ? `<tr><th>Require Cards</th><td>${selectedRun.hints.requireCards.join(", ")}</td></tr>` : ""}
922
+ ${selectedRun.hints.excludeTags ? `<tr><th>Exclude Tags</th><td>${selectedRun.hints.excludeTags.join(", ")}</td></tr>` : ""}
923
+ ${selectedRun.hints.excludeCards ? `<tr><th>Exclude Cards</th><td>${selectedRun.hints.excludeCards.join(", ")}</td></tr>` : ""}
924
+ </table>
925
+ ` : ""}
926
+
927
+ <h3>Filter Impact</h3>
928
+ <table>
929
+ <thead><tr><th>Filter</th><th>Boosted</th><th>Penalized</th><th>Passed</th><th>Removed</th></tr></thead>
930
+ <tbody>
931
+ ${selectedRun.filters.map(
932
+ (f) => `
933
+ <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>
934
+ `
935
+ ).join("")}
936
+ </tbody>
937
+ </table>
938
+
939
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
940
+ <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
941
+
942
+ <table>
943
+ <thead><tr><th>ID</th><th>Generator</th><th>Origin</th><th>Score</th><th>Selected</th></tr></thead>
944
+ <tbody>
945
+ ${filteredCards.map(
946
+ (c) => `
947
+ <tr>
948
+ <td><code>${c.cardId}</code></td>
949
+ <td>${c.generator || "unknown"}</td>
950
+ <td>${c.origin}</td>
951
+ <td>${c.finalScore.toFixed(3)}</td>
952
+ <td>${c.selected ? "\u2705" : "\u274C"}</td>
953
+ </tr>
954
+ ${c.selected || _cardSearchQuery ? `
955
+ <tr>
956
+ <td colspan="5">
957
+ <div class="provenance">${formatProvenance(c.provenance)}</div>
958
+ </td>
959
+ </tr>
960
+ ` : ""}
961
+ `
962
+ ).join("")}
963
+ </tbody>
964
+ </table>
965
+ `;
966
+ }
967
+ _uiContainer.innerHTML = `
968
+ <style>${styles}</style>
969
+ <header>
970
+ <strong>Pipeline Debugger</strong>
971
+ <button class="close-btn" onclick="window.skuilder.pipeline.ui()">Close</button>
972
+ </header>
973
+ <div class="container">
974
+ <div class="sidebar">${runListHtml}</div>
975
+ <div class="main-content">${detailsHtml}</div>
976
+ </div>
977
+ `;
978
+ }
820
979
  function mountPipelineDebugger() {
821
980
  if (typeof window === "undefined") return;
822
981
  const win = window;
823
982
  win.skuilder = win.skuilder || {};
824
983
  win.skuilder.pipeline = pipelineDebugAPI;
825
984
  }
826
- var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
985
+ var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
827
986
  var init_PipelineDebugger = __esm({
828
987
  "src/core/navigators/PipelineDebugger.ts"() {
829
988
  "use strict";
@@ -832,6 +991,9 @@ var init_PipelineDebugger = __esm({
832
991
  _activePipeline = null;
833
992
  MAX_RUNS = 10;
834
993
  runHistory = [];
994
+ _uiContainer = null;
995
+ _selectedRunIndex = null;
996
+ _cardSearchQuery = "";
835
997
  pipelineDebugAPI = {
836
998
  /**
837
999
  * Get raw run history for programmatic access.
@@ -971,16 +1133,20 @@ var init_PipelineDebugger = __esm({
971
1133
  const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
972
1134
  const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
973
1135
  const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
1136
+ const supportCard = reason.match(/supportCard=([^;]+)/)?.[1] ?? "none";
974
1137
  const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
975
1138
  const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
1139
+ const supportSource = mode === "discovered-support" ? "discovered" : mode === "support" ? "authored" : "n/a";
976
1140
  return {
977
1141
  group: parsedGroup,
978
1142
  mode,
1143
+ supportSource,
979
1144
  cardId: card.cardId,
980
1145
  selected: card.selected ? "yes" : "no",
981
1146
  finalScore: card.finalScore.toFixed(3),
982
1147
  blocked,
983
1148
  blockedTargets,
1149
+ supportCard,
984
1150
  supportTags,
985
1151
  multiplier
986
1152
  };
@@ -996,6 +1162,8 @@ var init_PipelineDebugger = __esm({
996
1162
  const selectedRows = rows.filter((r) => r.selected === "yes");
997
1163
  const blockedTargetSet = /* @__PURE__ */ new Set();
998
1164
  const supportTagSet = /* @__PURE__ */ new Set();
1165
+ const authoredSupportSet = /* @__PURE__ */ new Set();
1166
+ const discoveredSupportSet = /* @__PURE__ */ new Set();
999
1167
  for (const row of rows) {
1000
1168
  if (row.blockedTargets && row.blockedTargets !== "none") {
1001
1169
  row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
@@ -1003,6 +1171,13 @@ var init_PipelineDebugger = __esm({
1003
1171
  if (row.supportTags && row.supportTags !== "none") {
1004
1172
  row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
1005
1173
  }
1174
+ if (row.supportCard && row.supportCard !== "none") {
1175
+ if (row.supportSource === "discovered") {
1176
+ discoveredSupportSet.add(row.supportCard);
1177
+ } else if (row.supportSource === "authored") {
1178
+ authoredSupportSet.add(row.supportCard);
1179
+ }
1180
+ }
1006
1181
  }
1007
1182
  logger.info(`Prescribed cards in run: ${rows.length}`);
1008
1183
  logger.info(`Selected prescribed cards: ${selectedRows.length}`);
@@ -1012,6 +1187,12 @@ var init_PipelineDebugger = __esm({
1012
1187
  logger.info(
1013
1188
  `Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
1014
1189
  );
1190
+ logger.info(
1191
+ `Authored support cards emitted: ${authoredSupportSet.size > 0 ? [...authoredSupportSet].join(", ") : "none"}`
1192
+ );
1193
+ logger.info(
1194
+ `Discovered support cards emitted: ${discoveredSupportSet.size > 0 ? [...discoveredSupportSet].join(", ") : "none"}`
1195
+ );
1015
1196
  console.groupEnd();
1016
1197
  },
1017
1198
  /**
@@ -1146,6 +1327,39 @@ var init_PipelineDebugger = __esm({
1146
1327
  Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
1147
1328
  );
1148
1329
  },
1330
+ /**
1331
+ * Toggle the full-screen UI debugger.
1332
+ */
1333
+ ui() {
1334
+ if (_uiContainer) {
1335
+ document.body.removeChild(_uiContainer);
1336
+ _uiContainer = null;
1337
+ return;
1338
+ }
1339
+ _uiContainer = document.createElement("div");
1340
+ _uiContainer.id = "sk-pipeline-debugger";
1341
+ document.body.appendChild(_uiContainer);
1342
+ if (_selectedRunIndex === null && runHistory.length > 0) {
1343
+ _selectedRunIndex = 0;
1344
+ }
1345
+ renderUI();
1346
+ },
1347
+ /**
1348
+ * Internal UI helpers
1349
+ * @internal
1350
+ */
1351
+ _selectRun(index) {
1352
+ _selectedRunIndex = index;
1353
+ renderUI();
1354
+ },
1355
+ /**
1356
+ * Internal UI helpers
1357
+ * @internal
1358
+ */
1359
+ _setSearch(query) {
1360
+ _cardSearchQuery = query;
1361
+ renderUI();
1362
+ },
1149
1363
  /**
1150
1364
  * Show help.
1151
1365
  */
@@ -1154,6 +1368,7 @@ var init_PipelineDebugger = __esm({
1154
1368
  \u{1F527} Pipeline Debug API
1155
1369
 
1156
1370
  Commands:
1371
+ .ui() Toggle full-screen UI debugger
1157
1372
  .showLastRun() Show summary of most recent pipeline run
1158
1373
  .showRun(id|index) Show summary of a specific run (by index or ID suffix)
1159
1374
  .showCard(cardId) Show provenance trail for a specific card
@@ -1170,6 +1385,7 @@ Commands:
1170
1385
  .help() Show this help message
1171
1386
 
1172
1387
  Example:
1388
+ window.skuilder.pipeline.ui()
1173
1389
  window.skuilder.pipeline.showLastRun()
1174
1390
  window.skuilder.pipeline.showRun(1)
1175
1391
  await window.skuilder.pipeline.diagnoseCardSpace()
@@ -1332,7 +1548,7 @@ var init_CompositeGenerator = __esm({
1332
1548
  for (const [, items] of byCardId) {
1333
1549
  const cards2 = items.map((i) => i.card);
1334
1550
  const aggregatedScore = this.aggregateScores(items);
1335
- const finalScore = Math.min(1, aggregatedScore);
1551
+ const finalScore = Math.max(0, aggregatedScore);
1336
1552
  const mergedProvenance = cards2.flatMap((c) => c.provenance);
1337
1553
  const initialScore = cards2[0].score;
1338
1554
  const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
@@ -1529,10 +1745,26 @@ function matchesTagPattern(tag, pattern) {
1529
1745
  const re = new RegExp(`^${escaped}$`);
1530
1746
  return re.test(tag);
1531
1747
  }
1748
+ function extractWordStem(cardId) {
1749
+ for (const prefix of ["c-ml-", "c-ws-", "c-spelling-"]) {
1750
+ if (cardId.startsWith(prefix)) {
1751
+ const rest = cardId.slice(prefix.length);
1752
+ const lastDash = rest.lastIndexOf("-");
1753
+ return lastDash > 0 ? rest.slice(0, lastDash) : rest;
1754
+ }
1755
+ }
1756
+ return cardId;
1757
+ }
1758
+ function shuffleInPlace(arr) {
1759
+ for (let i = arr.length - 1; i > 0; i--) {
1760
+ const j = Math.floor(Math.random() * (i + 1));
1761
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1762
+ }
1763
+ }
1532
1764
  function pickTopByScore(cards, limit) {
1533
1765
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1534
1766
  }
1535
- 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;
1767
+ 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;
1536
1768
  var init_prescribed = __esm({
1537
1769
  "src/core/navigators/generators/prescribed.ts"() {
1538
1770
  "use strict";
@@ -1545,11 +1777,10 @@ var init_prescribed = __esm({
1545
1777
  DEFAULT_MIN_COUNT = 3;
1546
1778
  BASE_TARGET_SCORE = 1;
1547
1779
  BASE_SUPPORT_SCORE = 0.8;
1780
+ DISCOVERED_SUPPORT_SCORE = 12;
1548
1781
  MAX_TARGET_MULTIPLIER = 8;
1549
1782
  MAX_SUPPORT_MULTIPLIER = 4;
1550
- LOCKED_TAG_PREFIXES = ["concept:"];
1551
- LESSON_GATE_PENALTY_TAG_HINT = "concept:";
1552
- PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
1783
+ PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
1553
1784
  PrescribedCardsGenerator = class extends ContentNavigator {
1554
1785
  name;
1555
1786
  config;
@@ -1585,6 +1816,20 @@ var init_prescribed = __esm({
1585
1816
  const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
1586
1817
  const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
1587
1818
  const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
1819
+ const courseTagDocs = await this.course.getCourseTagStubs().catch(
1820
+ () => ({
1821
+ rows: [],
1822
+ offset: 0,
1823
+ total_rows: 0
1824
+ })
1825
+ );
1826
+ const cardsByTag = /* @__PURE__ */ new Map();
1827
+ for (const row of courseTagDocs.rows ?? []) {
1828
+ const tagDoc = row.doc;
1829
+ if (tagDoc?.name && Array.isArray(tagDoc.taggedCards)) {
1830
+ cardsByTag.set(tagDoc.name, [...tagDoc.taggedCards]);
1831
+ }
1832
+ }
1588
1833
  const nextState = {
1589
1834
  updatedAt: isoNow(),
1590
1835
  groups: {}
@@ -1599,11 +1844,31 @@ var init_prescribed = __esm({
1599
1844
  activeIds,
1600
1845
  seenIds,
1601
1846
  tagsByCard,
1847
+ cardsByTag,
1602
1848
  hierarchyConfigs,
1603
1849
  userTagElo,
1604
1850
  userGlobalElo
1605
1851
  });
1606
1852
  groupRuntimes.push(runtime);
1853
+ logger.info(
1854
+ `[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)}`
1855
+ );
1856
+ if (runtime.blockedTargets.length > 0) {
1857
+ logger.info(
1858
+ `[Prescribed] Group '${group.id}' blocked targets: ${runtime.blockedTargets.join(", ")}`
1859
+ );
1860
+ logger.info(
1861
+ `[Prescribed] Group '${group.id}' support tags needed: ${runtime.supportTags.join(", ") || "(none)"}`
1862
+ );
1863
+ logger.info(
1864
+ `[Prescribed] Group '${group.id}' escalation mode: ` + (runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support-candidates" : "boost-only")
1865
+ );
1866
+ if (runtime.discoveredSupportCandidates.length > 0) {
1867
+ logger.info(
1868
+ `[Prescribed] Group '${group.id}' discovered support candidates: ${runtime.discoveredSupportCandidates.join(", ")}`
1869
+ );
1870
+ }
1871
+ }
1607
1872
  nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
1608
1873
  const directCards = this.buildDirectTargetCards(
1609
1874
  runtime,
@@ -1615,15 +1880,30 @@ var init_prescribed = __esm({
1615
1880
  courseId,
1616
1881
  emittedIds
1617
1882
  );
1618
- emitted.push(...directCards, ...supportCards);
1883
+ const discoveredSupportCards = this.buildDiscoveredSupportCards(
1884
+ runtime,
1885
+ courseId,
1886
+ emittedIds
1887
+ );
1888
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
1619
1889
  }
1620
1890
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1621
1891
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
1622
1892
  boostTags: hintSummary.boostTags,
1623
1893
  _label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
1624
1894
  } : void 0;
1895
+ if (hints) {
1896
+ const tagEntries = Object.entries(hints.boostTags ?? {});
1897
+ logger.info(
1898
+ `[Prescribed] Emitting ${tagEntries.length} boost hint(s): ` + tagEntries.map(([tag, mult]) => `${tag}\xD7${mult.toFixed(1)}`).join(", ")
1899
+ );
1900
+ } else {
1901
+ logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
1902
+ }
1625
1903
  if (emitted.length === 0) {
1626
- logger.debug("[Prescribed] No prescribed targets/support emitted this run");
1904
+ logger.info(
1905
+ "[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
1906
+ );
1627
1907
  await this.putStrategyState(nextState).catch((e) => {
1628
1908
  logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
1629
1909
  });
@@ -1656,7 +1936,7 @@ var init_prescribed = __esm({
1656
1936
  logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
1657
1937
  });
1658
1938
  logger.info(
1659
- `[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)`
1939
+ `[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)`
1660
1940
  );
1661
1941
  return hints ? { cards: finalCards, hints } : { cards: finalCards };
1662
1942
  }
@@ -1686,9 +1966,15 @@ var init_prescribed = __esm({
1686
1966
  const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
1687
1967
  const groups = groupsRaw.map((raw, i) => ({
1688
1968
  id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
1689
- targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
1690
- supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
1691
- supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
1969
+ targetCardIds: dedupe(
1970
+ Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []
1971
+ ),
1972
+ supportCardIds: dedupe(
1973
+ Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []
1974
+ ),
1975
+ supportTagPatterns: dedupe(
1976
+ Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []
1977
+ ),
1692
1978
  freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
1693
1979
  maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
1694
1980
  maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
@@ -1705,7 +1991,7 @@ var init_prescribed = __esm({
1705
1991
  }
1706
1992
  async loadHierarchyConfigs() {
1707
1993
  try {
1708
- const strategies = await this.course.getNavigationStrategies();
1994
+ const strategies = await this.course.getAllNavigationStrategies();
1709
1995
  return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
1710
1996
  try {
1711
1997
  const parsed = JSON.parse(s.serializedData);
@@ -1728,6 +2014,7 @@ var init_prescribed = __esm({
1728
2014
  activeIds,
1729
2015
  seenIds,
1730
2016
  tagsByCard,
2017
+ cardsByTag,
1731
2018
  hierarchyConfigs,
1732
2019
  userTagElo,
1733
2020
  userGlobalElo
@@ -1791,6 +2078,22 @@ var init_prescribed = __esm({
1791
2078
  [...supportTags]
1792
2079
  )
1793
2080
  ]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
2081
+ const discoveredSupportCandidates = blockedTargets.length > 0 && supportTags.size > 0 && supportCandidates.length === 0 ? this.findDiscoveredSupportCards({
2082
+ supportTags: [...supportTags],
2083
+ cardsByTag,
2084
+ activeIds,
2085
+ seenIds,
2086
+ excludedIds: /* @__PURE__ */ new Set([
2087
+ ...group.targetCardIds,
2088
+ ...group.supportCardIds ?? []
2089
+ ]),
2090
+ limit: group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN
2091
+ }) : [];
2092
+ if (blockedTargets.length > 0 && supportTags.size > 0 && discoveredSupportCandidates.length === 0) {
2093
+ logger.info(
2094
+ `[Prescribed] Group '${group.id}' discovered 0 broader support candidates (blocked=${blockedTargets.length}; authoredSupport=${supportCandidates.length})`
2095
+ );
2096
+ }
1794
2097
  const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
1795
2098
  const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
1796
2099
  const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
@@ -1804,6 +2107,7 @@ var init_prescribed = __esm({
1804
2107
  surfaceableTargets,
1805
2108
  targetTags,
1806
2109
  supportCandidates,
2110
+ discoveredSupportCandidates,
1807
2111
  supportTags: [...supportTags],
1808
2112
  pressureMultiplier,
1809
2113
  supportMultiplier,
@@ -1874,6 +2178,33 @@ var init_prescribed = __esm({
1874
2178
  }
1875
2179
  return cards;
1876
2180
  }
2181
+ buildDiscoveredSupportCards(runtime, courseId, emittedIds) {
2182
+ if (runtime.blockedTargets.length === 0 || runtime.discoveredSupportCandidates.length === 0) {
2183
+ return [];
2184
+ }
2185
+ const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
2186
+ const supportIds = runtime.discoveredSupportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
2187
+ const cards = [];
2188
+ for (const cardId of supportIds) {
2189
+ emittedIds.add(cardId);
2190
+ cards.push({
2191
+ cardId,
2192
+ courseId,
2193
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
2194
+ provenance: [
2195
+ {
2196
+ strategy: "prescribed",
2197
+ strategyName: this.strategyName || this.name,
2198
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2199
+ action: "generated",
2200
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
2201
+ 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}`
2202
+ }
2203
+ ]
2204
+ });
2205
+ }
2206
+ return cards;
2207
+ }
1877
2208
  findSupportCardsByTags(group, tagsByCard, supportTags) {
1878
2209
  if (supportTags.length === 0) {
1879
2210
  return [];
@@ -1896,6 +2227,40 @@ var init_prescribed = __esm({
1896
2227
  }
1897
2228
  return [...candidates];
1898
2229
  }
2230
+ findDiscoveredSupportCards(args) {
2231
+ const { supportTags, cardsByTag, activeIds, seenIds, excludedIds, limit } = args;
2232
+ const byCardId = /* @__PURE__ */ new Map();
2233
+ for (const supportTag of supportTags) {
2234
+ const taggedCards = cardsByTag.get(supportTag) ?? [];
2235
+ for (const cardId of taggedCards) {
2236
+ if (activeIds.has(cardId) || seenIds.has(cardId) || excludedIds.has(cardId)) {
2237
+ continue;
2238
+ }
2239
+ const existing = byCardId.get(cardId);
2240
+ if (existing) {
2241
+ existing.matches += 1;
2242
+ } else {
2243
+ byCardId.set(cardId, { cardId, matches: 1 });
2244
+ }
2245
+ }
2246
+ }
2247
+ const candidates = [...byCardId.values()].sort((a, b) => b.matches - a.matches || a.cardId.localeCompare(b.cardId));
2248
+ const usedStems = /* @__PURE__ */ new Set();
2249
+ const diverse = [];
2250
+ const deferred = [];
2251
+ for (const entry of candidates) {
2252
+ const stem = extractWordStem(entry.cardId);
2253
+ if (!usedStems.has(stem)) {
2254
+ usedStems.add(stem);
2255
+ diverse.push(entry);
2256
+ } else {
2257
+ deferred.push(entry);
2258
+ }
2259
+ }
2260
+ shuffleInPlace(diverse);
2261
+ shuffleInPlace(deferred);
2262
+ return [...diverse, ...deferred].slice(0, limit).map((entry) => entry.cardId);
2263
+ }
1899
2264
  resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
1900
2265
  const supportTags = /* @__PURE__ */ new Set();
1901
2266
  let blocked = false;
@@ -1941,7 +2306,6 @@ var init_prescribed = __esm({
1941
2306
  }
1942
2307
  collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
1943
2308
  if (depth < 0 || visited.has(tag)) return;
1944
- if (this.isHardGatedTag(tag)) return;
1945
2309
  visited.add(tag);
1946
2310
  let walkedFurther = false;
1947
2311
  for (const hierarchy of hierarchyConfigs) {
@@ -1969,9 +2333,6 @@ var init_prescribed = __esm({
1969
2333
  out.add(tag);
1970
2334
  }
1971
2335
  }
1972
- isHardGatedTag(tag) {
1973
- return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
1974
- }
1975
2336
  isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
1976
2337
  if (!userTagElo) return false;
1977
2338
  const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
@@ -3763,6 +4124,32 @@ var init_Pipeline = __esm({
3763
4124
  cards = await this.hydrateTags(cards);
3764
4125
  const tHydrate = performance.now();
3765
4126
  const allCardsBeforeFiltering = [...cards];
4127
+ const pendingHints = this._ephemeralHints;
4128
+ if (pendingHints?.requireCards?.length) {
4129
+ const poolIds = new Set(allCardsBeforeFiltering.map((c) => c.cardId));
4130
+ const missingIds = pendingHints.requireCards.filter(
4131
+ (p) => !p.includes("*") && !poolIds.has(p)
4132
+ );
4133
+ if (missingIds.length > 0) {
4134
+ const fetchedTags = await this.course.getAppliedTagsBatch(missingIds);
4135
+ const courseId = this.course.getCourseID();
4136
+ for (const cardId of missingIds) {
4137
+ allCardsBeforeFiltering.push({
4138
+ cardId,
4139
+ courseId,
4140
+ score: 1,
4141
+ tags: fetchedTags.get(cardId) ?? [],
4142
+ provenance: []
4143
+ });
4144
+ }
4145
+ logger.info(
4146
+ `[Pipeline] Pre-fetched ${missingIds.length} required card(s) into pool: ${missingIds.join(", ")}`
4147
+ );
4148
+ }
4149
+ }
4150
+ const prescribedIds = new Set(
4151
+ cards.filter((c) => c.provenance.some((p) => p.strategy === "prescribed")).map((c) => c.cardId)
4152
+ );
3766
4153
  const filterImpacts = [];
3767
4154
  for (const filter of this.filters) {
3768
4155
  const beforeCount = cards.length;
@@ -3777,6 +4164,17 @@ var init_Pipeline = __esm({
3777
4164
  else passed++;
3778
4165
  }
3779
4166
  filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
4167
+ if (prescribedIds.size > 0) {
4168
+ const survivingIds = new Set(cards.map((c) => c.cardId));
4169
+ const killedPrescribed = [...prescribedIds].filter((id) => !survivingIds.has(id));
4170
+ const zeroedPrescribed = cards.filter((c) => prescribedIds.has(c.cardId) && c.score === 0).map((c) => c.cardId);
4171
+ if (killedPrescribed.length > 0 || zeroedPrescribed.length > 0) {
4172
+ logger.info(
4173
+ `[Pipeline] Filter '${filter.name}' impact on prescribed cards: ` + (killedPrescribed.length > 0 ? `removed=[${killedPrescribed.join(", ")}] ` : "") + (zeroedPrescribed.length > 0 ? `zeroed=[${zeroedPrescribed.join(", ")}]` : "")
4174
+ );
4175
+ killedPrescribed.forEach((id) => prescribedIds.delete(id));
4176
+ }
4177
+ }
3780
4178
  logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
3781
4179
  }
3782
4180
  cards = cards.filter((c) => c.score > 0);
@@ -3813,7 +4211,8 @@ var init_Pipeline = __esm({
3813
4211
  filterImpacts,
3814
4212
  cards,
3815
4213
  result,
3816
- context.userElo
4214
+ context.userElo,
4215
+ hints ?? void 0
3817
4216
  );
3818
4217
  captureRun(report);
3819
4218
  } catch (e) {
@@ -3923,13 +4322,27 @@ var init_Pipeline = __esm({
3923
4322
  }
3924
4323
  }
3925
4324
  const cardIds = new Set(cards.map((c) => c.cardId));
4325
+ const cardMap = new Map(cards.map((c) => [c.cardId, c]));
3926
4326
  const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
3927
- const inject = (card, reason) => {
3928
- if (!cardIds.has(card.cardId)) {
3929
- const floorScore = Math.max(card.score, 1);
4327
+ const applyRequirement = (card, reason) => {
4328
+ const mandatoryScore = Number.POSITIVE_INFINITY;
4329
+ const existing = cardMap.get(card.cardId);
4330
+ if (existing) {
4331
+ if (existing.score < mandatoryScore) {
4332
+ existing.score = mandatoryScore;
4333
+ existing.provenance.push({
4334
+ strategy: "ephemeralHint",
4335
+ strategyId: "ephemeral-hint",
4336
+ strategyName: hintLabel,
4337
+ action: "boosted",
4338
+ score: mandatoryScore,
4339
+ reason: `${reason} (upgrade to mandatory score)`
4340
+ });
4341
+ }
4342
+ } else {
3930
4343
  cards.push({
3931
4344
  ...card,
3932
- score: floorScore,
4345
+ score: mandatoryScore,
3933
4346
  provenance: [
3934
4347
  ...card.provenance,
3935
4348
  {
@@ -3937,25 +4350,41 @@ var init_Pipeline = __esm({
3937
4350
  strategyId: "ephemeral-hint",
3938
4351
  strategyName: hintLabel,
3939
4352
  action: "boosted",
3940
- score: floorScore,
4353
+ score: mandatoryScore,
3941
4354
  reason
3942
4355
  }
3943
4356
  ]
3944
4357
  });
3945
4358
  cardIds.add(card.cardId);
4359
+ cardMap.set(card.cardId, cards[cards.length - 1]);
3946
4360
  }
3947
4361
  };
3948
4362
  if (hints.requireCards?.length) {
3949
4363
  for (const pattern of hints.requireCards) {
4364
+ for (const cardId of cardIds) {
4365
+ if (globMatch(cardId, pattern)) {
4366
+ applyRequirement(cardMap.get(cardId), `requireCard ${pattern}`);
4367
+ }
4368
+ }
3950
4369
  for (const card of allCards) {
3951
- if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
4370
+ if (globMatch(card.cardId, pattern)) {
4371
+ applyRequirement(card, `requireCard ${pattern}`);
4372
+ }
3952
4373
  }
3953
4374
  }
3954
4375
  }
3955
4376
  if (hints.requireTags?.length) {
3956
4377
  for (const pattern of hints.requireTags) {
4378
+ for (const cardId of cardIds) {
4379
+ const card = cardMap.get(cardId);
4380
+ if (cardMatchesTagPattern(card, pattern)) {
4381
+ applyRequirement(card, `requireTag ${pattern}`);
4382
+ }
4383
+ }
3957
4384
  for (const card of allCards) {
3958
- if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
4385
+ if (cardMatchesTagPattern(card, pattern)) {
4386
+ applyRequirement(card, `requireTag ${pattern}`);
4387
+ }
3959
4388
  }
3960
4389
  }
3961
4390
  }