@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.
@@ -538,8 +538,9 @@ function getOrigin(card) {
538
538
  const firstEntry = card.provenance[0];
539
539
  if (!firstEntry) return "unknown";
540
540
  const reason = firstEntry.reason?.toLowerCase() || "";
541
- if (reason.includes("new card")) return "new";
542
- if (reason.includes("review")) return "review";
541
+ const strategy = firstEntry.strategy?.toLowerCase() || "";
542
+ if (reason.includes("new card") || strategy.includes("elo")) return "new";
543
+ if (reason.includes("review") || strategy.includes("srs")) return "review";
543
544
  return "unknown";
544
545
  }
545
546
  function captureRun(report) {
@@ -559,12 +560,13 @@ function parseCardElo(provenance) {
559
560
  const match = eloEntry.reason.match(/card:\s*(\d+)/);
560
561
  return match ? parseInt(match[1], 10) : void 0;
561
562
  }
562
- function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
563
+ function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
563
564
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
564
565
  const cards = allCards.map((card) => ({
565
566
  cardId: card.cardId,
566
567
  courseId: card.courseId,
567
568
  origin: getOrigin(card),
569
+ generator: card.provenance[0]?.strategyName || card.provenance[0]?.strategy,
568
570
  finalScore: card.score,
569
571
  cardElo: parseCardElo(card.provenance),
570
572
  provenance: card.provenance,
@@ -581,6 +583,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
581
583
  generators,
582
584
  generatedCount,
583
585
  filters,
586
+ hints,
584
587
  finalCount: selectedCards.length,
585
588
  reviewsSelected,
586
589
  newSelected,
@@ -620,13 +623,169 @@ function printRunSummary(run) {
620
623
  );
621
624
  console.groupEnd();
622
625
  }
626
+ function renderUI() {
627
+ if (!_uiContainer) return;
628
+ const runs = runHistory;
629
+ const selectedRun = _selectedRunIndex !== null ? runs[_selectedRunIndex] : null;
630
+ const styles = `
631
+ #sk-pipeline-debugger {
632
+ position: fixed;
633
+ top: 0;
634
+ left: 0;
635
+ width: 100vw;
636
+ height: 100vh;
637
+ background: #f8f9fa;
638
+ color: #212529;
639
+ z-index: 999999;
640
+ display: flex;
641
+ flex-direction: column;
642
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
643
+ font-size: 14px;
644
+ }
645
+ #sk-pipeline-debugger header {
646
+ padding: 1rem;
647
+ background: #343a40;
648
+ color: white;
649
+ display: flex;
650
+ justify-content: space-between;
651
+ align-items: center;
652
+ }
653
+ #sk-pipeline-debugger .container {
654
+ display: flex;
655
+ flex: 1;
656
+ overflow: hidden;
657
+ }
658
+ #sk-pipeline-debugger .sidebar {
659
+ width: 300px;
660
+ border-right: 1px solid #dee2e6;
661
+ overflow-y: auto;
662
+ background: white;
663
+ }
664
+ #sk-pipeline-debugger .main-content {
665
+ flex: 1;
666
+ overflow-y: auto;
667
+ padding: 1.5rem;
668
+ }
669
+ #sk-pipeline-debugger .run-item {
670
+ padding: 0.75rem 1rem;
671
+ border-bottom: 1px solid #eee;
672
+ cursor: pointer;
673
+ }
674
+ #sk-pipeline-debugger .run-item:hover { background: #f1f3f5; }
675
+ #sk-pipeline-debugger .run-item.active { background: #e9ecef; border-left: 4px solid #007bff; }
676
+ #sk-pipeline-debugger h2, #sk-pipeline-debugger h3 { margin-top: 0; }
677
+ #sk-pipeline-debugger table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; background: white; }
678
+ #sk-pipeline-debugger th, #sk-pipeline-debugger td { border: 1px solid #dee2e6; padding: 0.5rem; text-align: left; }
679
+ #sk-pipeline-debugger th { background: #f1f3f5; }
680
+ #sk-pipeline-debugger code { background: #f1f3f5; padding: 0.1rem 0.3rem; border-radius: 3px; font-family: monospace; }
681
+ #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
682
+ #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
683
+ #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; }
684
+ `;
685
+ const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
686
+ (r, i) => `
687
+ <div class="run-item ${i === _selectedRunIndex ? "active" : ""}" onclick="window.skuilder.pipeline._selectRun(${i})">
688
+ <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
689
+ <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
690
+ <small>${r.finalCount} cards selected</small>
691
+ </div>
692
+ `
693
+ ).join("");
694
+ let detailsHtml = '<div style="color: #6c757d; text-align: center; margin-top: 5rem;">Select a run to see details</div>';
695
+ if (selectedRun) {
696
+ const filteredCards = selectedRun.cards.filter(
697
+ (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
698
+ );
699
+ detailsHtml = `
700
+ <h2>Run: ${selectedRun.runId}</h2>
701
+ <p>
702
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
703
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
704
+ <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
705
+ </p>
706
+
707
+ <h3>Pipeline Config</h3>
708
+ <table>
709
+ <tr><th>Generator</th><td>${selectedRun.generatorName} (${selectedRun.generatedCount} candidates)</td></tr>
710
+ ${(selectedRun.generators || []).map(
711
+ (g) => `
712
+ <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>
713
+ `
714
+ ).join("")}
715
+ </table>
716
+
717
+ ${selectedRun.hints ? `
718
+ <h3>Ephemeral Hints</h3>
719
+ <table>
720
+ ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
721
+ ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
722
+ ${selectedRun.hints.boostCards ? `<tr><th>Boost Cards</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostCards, null, 2)}</pre></td></tr>` : ""}
723
+ ${selectedRun.hints.requireTags ? `<tr><th>Require Tags</th><td>${selectedRun.hints.requireTags.join(", ")}</td></tr>` : ""}
724
+ ${selectedRun.hints.requireCards ? `<tr><th>Require Cards</th><td>${selectedRun.hints.requireCards.join(", ")}</td></tr>` : ""}
725
+ ${selectedRun.hints.excludeTags ? `<tr><th>Exclude Tags</th><td>${selectedRun.hints.excludeTags.join(", ")}</td></tr>` : ""}
726
+ ${selectedRun.hints.excludeCards ? `<tr><th>Exclude Cards</th><td>${selectedRun.hints.excludeCards.join(", ")}</td></tr>` : ""}
727
+ </table>
728
+ ` : ""}
729
+
730
+ <h3>Filter Impact</h3>
731
+ <table>
732
+ <thead><tr><th>Filter</th><th>Boosted</th><th>Penalized</th><th>Passed</th><th>Removed</th></tr></thead>
733
+ <tbody>
734
+ ${selectedRun.filters.map(
735
+ (f) => `
736
+ <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>
737
+ `
738
+ ).join("")}
739
+ </tbody>
740
+ </table>
741
+
742
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
743
+ <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
744
+
745
+ <table>
746
+ <thead><tr><th>ID</th><th>Generator</th><th>Origin</th><th>Score</th><th>Selected</th></tr></thead>
747
+ <tbody>
748
+ ${filteredCards.map(
749
+ (c) => `
750
+ <tr>
751
+ <td><code>${c.cardId}</code></td>
752
+ <td>${c.generator || "unknown"}</td>
753
+ <td>${c.origin}</td>
754
+ <td>${c.finalScore.toFixed(3)}</td>
755
+ <td>${c.selected ? "\u2705" : "\u274C"}</td>
756
+ </tr>
757
+ ${c.selected || _cardSearchQuery ? `
758
+ <tr>
759
+ <td colspan="5">
760
+ <div class="provenance">${formatProvenance(c.provenance)}</div>
761
+ </td>
762
+ </tr>
763
+ ` : ""}
764
+ `
765
+ ).join("")}
766
+ </tbody>
767
+ </table>
768
+ `;
769
+ }
770
+ _uiContainer.innerHTML = `
771
+ <style>${styles}</style>
772
+ <header>
773
+ <strong>Pipeline Debugger</strong>
774
+ <button class="close-btn" onclick="window.skuilder.pipeline.ui()">Close</button>
775
+ </header>
776
+ <div class="container">
777
+ <div class="sidebar">${runListHtml}</div>
778
+ <div class="main-content">${detailsHtml}</div>
779
+ </div>
780
+ `;
781
+ }
623
782
  function mountPipelineDebugger() {
624
783
  if (typeof window === "undefined") return;
625
784
  const win = window;
626
785
  win.skuilder = win.skuilder || {};
627
786
  win.skuilder.pipeline = pipelineDebugAPI;
628
787
  }
629
- var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
788
+ var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
630
789
  var init_PipelineDebugger = __esm({
631
790
  "src/core/navigators/PipelineDebugger.ts"() {
632
791
  "use strict";
@@ -635,6 +794,9 @@ var init_PipelineDebugger = __esm({
635
794
  _activePipeline = null;
636
795
  MAX_RUNS = 10;
637
796
  runHistory = [];
797
+ _uiContainer = null;
798
+ _selectedRunIndex = null;
799
+ _cardSearchQuery = "";
638
800
  pipelineDebugAPI = {
639
801
  /**
640
802
  * Get raw run history for programmatic access.
@@ -774,16 +936,20 @@ var init_PipelineDebugger = __esm({
774
936
  const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
775
937
  const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
776
938
  const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
939
+ const supportCard = reason.match(/supportCard=([^;]+)/)?.[1] ?? "none";
777
940
  const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
778
941
  const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
942
+ const supportSource = mode === "discovered-support" ? "discovered" : mode === "support" ? "authored" : "n/a";
779
943
  return {
780
944
  group: parsedGroup,
781
945
  mode,
946
+ supportSource,
782
947
  cardId: card.cardId,
783
948
  selected: card.selected ? "yes" : "no",
784
949
  finalScore: card.finalScore.toFixed(3),
785
950
  blocked,
786
951
  blockedTargets,
952
+ supportCard,
787
953
  supportTags,
788
954
  multiplier
789
955
  };
@@ -799,6 +965,8 @@ var init_PipelineDebugger = __esm({
799
965
  const selectedRows = rows.filter((r) => r.selected === "yes");
800
966
  const blockedTargetSet = /* @__PURE__ */ new Set();
801
967
  const supportTagSet = /* @__PURE__ */ new Set();
968
+ const authoredSupportSet = /* @__PURE__ */ new Set();
969
+ const discoveredSupportSet = /* @__PURE__ */ new Set();
802
970
  for (const row of rows) {
803
971
  if (row.blockedTargets && row.blockedTargets !== "none") {
804
972
  row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
@@ -806,6 +974,13 @@ var init_PipelineDebugger = __esm({
806
974
  if (row.supportTags && row.supportTags !== "none") {
807
975
  row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
808
976
  }
977
+ if (row.supportCard && row.supportCard !== "none") {
978
+ if (row.supportSource === "discovered") {
979
+ discoveredSupportSet.add(row.supportCard);
980
+ } else if (row.supportSource === "authored") {
981
+ authoredSupportSet.add(row.supportCard);
982
+ }
983
+ }
809
984
  }
810
985
  logger.info(`Prescribed cards in run: ${rows.length}`);
811
986
  logger.info(`Selected prescribed cards: ${selectedRows.length}`);
@@ -815,6 +990,12 @@ var init_PipelineDebugger = __esm({
815
990
  logger.info(
816
991
  `Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
817
992
  );
993
+ logger.info(
994
+ `Authored support cards emitted: ${authoredSupportSet.size > 0 ? [...authoredSupportSet].join(", ") : "none"}`
995
+ );
996
+ logger.info(
997
+ `Discovered support cards emitted: ${discoveredSupportSet.size > 0 ? [...discoveredSupportSet].join(", ") : "none"}`
998
+ );
818
999
  console.groupEnd();
819
1000
  },
820
1001
  /**
@@ -949,6 +1130,39 @@ var init_PipelineDebugger = __esm({
949
1130
  Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
950
1131
  );
951
1132
  },
1133
+ /**
1134
+ * Toggle the full-screen UI debugger.
1135
+ */
1136
+ ui() {
1137
+ if (_uiContainer) {
1138
+ document.body.removeChild(_uiContainer);
1139
+ _uiContainer = null;
1140
+ return;
1141
+ }
1142
+ _uiContainer = document.createElement("div");
1143
+ _uiContainer.id = "sk-pipeline-debugger";
1144
+ document.body.appendChild(_uiContainer);
1145
+ if (_selectedRunIndex === null && runHistory.length > 0) {
1146
+ _selectedRunIndex = 0;
1147
+ }
1148
+ renderUI();
1149
+ },
1150
+ /**
1151
+ * Internal UI helpers
1152
+ * @internal
1153
+ */
1154
+ _selectRun(index) {
1155
+ _selectedRunIndex = index;
1156
+ renderUI();
1157
+ },
1158
+ /**
1159
+ * Internal UI helpers
1160
+ * @internal
1161
+ */
1162
+ _setSearch(query) {
1163
+ _cardSearchQuery = query;
1164
+ renderUI();
1165
+ },
952
1166
  /**
953
1167
  * Show help.
954
1168
  */
@@ -957,6 +1171,7 @@ var init_PipelineDebugger = __esm({
957
1171
  \u{1F527} Pipeline Debug API
958
1172
 
959
1173
  Commands:
1174
+ .ui() Toggle full-screen UI debugger
960
1175
  .showLastRun() Show summary of most recent pipeline run
961
1176
  .showRun(id|index) Show summary of a specific run (by index or ID suffix)
962
1177
  .showCard(cardId) Show provenance trail for a specific card
@@ -973,6 +1188,7 @@ Commands:
973
1188
  .help() Show this help message
974
1189
 
975
1190
  Example:
1191
+ window.skuilder.pipeline.ui()
976
1192
  window.skuilder.pipeline.showLastRun()
977
1193
  window.skuilder.pipeline.showRun(1)
978
1194
  await window.skuilder.pipeline.diagnoseCardSpace()
@@ -1135,7 +1351,7 @@ var init_CompositeGenerator = __esm({
1135
1351
  for (const [, items] of byCardId) {
1136
1352
  const cards2 = items.map((i) => i.card);
1137
1353
  const aggregatedScore = this.aggregateScores(items);
1138
- const finalScore = Math.min(1, aggregatedScore);
1354
+ const finalScore = Math.max(0, aggregatedScore);
1139
1355
  const mergedProvenance = cards2.flatMap((c) => c.provenance);
1140
1356
  const initialScore = cards2[0].score;
1141
1357
  const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
@@ -1332,10 +1548,26 @@ function matchesTagPattern(tag, pattern) {
1332
1548
  const re = new RegExp(`^${escaped}$`);
1333
1549
  return re.test(tag);
1334
1550
  }
1551
+ function extractWordStem(cardId) {
1552
+ for (const prefix of ["c-ml-", "c-ws-", "c-spelling-"]) {
1553
+ if (cardId.startsWith(prefix)) {
1554
+ const rest = cardId.slice(prefix.length);
1555
+ const lastDash = rest.lastIndexOf("-");
1556
+ return lastDash > 0 ? rest.slice(0, lastDash) : rest;
1557
+ }
1558
+ }
1559
+ return cardId;
1560
+ }
1561
+ function shuffleInPlace(arr) {
1562
+ for (let i = arr.length - 1; i > 0; i--) {
1563
+ const j = Math.floor(Math.random() * (i + 1));
1564
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1565
+ }
1566
+ }
1335
1567
  function pickTopByScore(cards, limit) {
1336
1568
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1337
1569
  }
1338
- 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;
1570
+ 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;
1339
1571
  var init_prescribed = __esm({
1340
1572
  "src/core/navigators/generators/prescribed.ts"() {
1341
1573
  "use strict";
@@ -1348,11 +1580,10 @@ var init_prescribed = __esm({
1348
1580
  DEFAULT_MIN_COUNT = 3;
1349
1581
  BASE_TARGET_SCORE = 1;
1350
1582
  BASE_SUPPORT_SCORE = 0.8;
1583
+ DISCOVERED_SUPPORT_SCORE = 12;
1351
1584
  MAX_TARGET_MULTIPLIER = 8;
1352
1585
  MAX_SUPPORT_MULTIPLIER = 4;
1353
- LOCKED_TAG_PREFIXES = ["concept:"];
1354
- LESSON_GATE_PENALTY_TAG_HINT = "concept:";
1355
- PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
1586
+ PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
1356
1587
  PrescribedCardsGenerator = class extends ContentNavigator {
1357
1588
  name;
1358
1589
  config;
@@ -1388,6 +1619,20 @@ var init_prescribed = __esm({
1388
1619
  const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
1389
1620
  const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
1390
1621
  const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
1622
+ const courseTagDocs = await this.course.getCourseTagStubs().catch(
1623
+ () => ({
1624
+ rows: [],
1625
+ offset: 0,
1626
+ total_rows: 0
1627
+ })
1628
+ );
1629
+ const cardsByTag = /* @__PURE__ */ new Map();
1630
+ for (const row of courseTagDocs.rows ?? []) {
1631
+ const tagDoc = row.doc;
1632
+ if (tagDoc?.name && Array.isArray(tagDoc.taggedCards)) {
1633
+ cardsByTag.set(tagDoc.name, [...tagDoc.taggedCards]);
1634
+ }
1635
+ }
1391
1636
  const nextState = {
1392
1637
  updatedAt: isoNow(),
1393
1638
  groups: {}
@@ -1402,11 +1647,31 @@ var init_prescribed = __esm({
1402
1647
  activeIds,
1403
1648
  seenIds,
1404
1649
  tagsByCard,
1650
+ cardsByTag,
1405
1651
  hierarchyConfigs,
1406
1652
  userTagElo,
1407
1653
  userGlobalElo
1408
1654
  });
1409
1655
  groupRuntimes.push(runtime);
1656
+ logger.info(
1657
+ `[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)}`
1658
+ );
1659
+ if (runtime.blockedTargets.length > 0) {
1660
+ logger.info(
1661
+ `[Prescribed] Group '${group.id}' blocked targets: ${runtime.blockedTargets.join(", ")}`
1662
+ );
1663
+ logger.info(
1664
+ `[Prescribed] Group '${group.id}' support tags needed: ${runtime.supportTags.join(", ") || "(none)"}`
1665
+ );
1666
+ logger.info(
1667
+ `[Prescribed] Group '${group.id}' escalation mode: ` + (runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support-candidates" : "boost-only")
1668
+ );
1669
+ if (runtime.discoveredSupportCandidates.length > 0) {
1670
+ logger.info(
1671
+ `[Prescribed] Group '${group.id}' discovered support candidates: ${runtime.discoveredSupportCandidates.join(", ")}`
1672
+ );
1673
+ }
1674
+ }
1410
1675
  nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
1411
1676
  const directCards = this.buildDirectTargetCards(
1412
1677
  runtime,
@@ -1418,15 +1683,30 @@ var init_prescribed = __esm({
1418
1683
  courseId,
1419
1684
  emittedIds
1420
1685
  );
1421
- emitted.push(...directCards, ...supportCards);
1686
+ const discoveredSupportCards = this.buildDiscoveredSupportCards(
1687
+ runtime,
1688
+ courseId,
1689
+ emittedIds
1690
+ );
1691
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
1422
1692
  }
1423
1693
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1424
1694
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
1425
1695
  boostTags: hintSummary.boostTags,
1426
1696
  _label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
1427
1697
  } : void 0;
1698
+ if (hints) {
1699
+ const tagEntries = Object.entries(hints.boostTags ?? {});
1700
+ logger.info(
1701
+ `[Prescribed] Emitting ${tagEntries.length} boost hint(s): ` + tagEntries.map(([tag, mult]) => `${tag}\xD7${mult.toFixed(1)}`).join(", ")
1702
+ );
1703
+ } else {
1704
+ logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
1705
+ }
1428
1706
  if (emitted.length === 0) {
1429
- logger.debug("[Prescribed] No prescribed targets/support emitted this run");
1707
+ logger.info(
1708
+ "[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
1709
+ );
1430
1710
  await this.putStrategyState(nextState).catch((e) => {
1431
1711
  logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
1432
1712
  });
@@ -1459,7 +1739,7 @@ var init_prescribed = __esm({
1459
1739
  logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
1460
1740
  });
1461
1741
  logger.info(
1462
- `[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)`
1742
+ `[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)`
1463
1743
  );
1464
1744
  return hints ? { cards: finalCards, hints } : { cards: finalCards };
1465
1745
  }
@@ -1489,9 +1769,15 @@ var init_prescribed = __esm({
1489
1769
  const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
1490
1770
  const groups = groupsRaw.map((raw, i) => ({
1491
1771
  id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
1492
- targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
1493
- supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
1494
- supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
1772
+ targetCardIds: dedupe(
1773
+ Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []
1774
+ ),
1775
+ supportCardIds: dedupe(
1776
+ Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []
1777
+ ),
1778
+ supportTagPatterns: dedupe(
1779
+ Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []
1780
+ ),
1495
1781
  freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
1496
1782
  maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
1497
1783
  maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
@@ -1508,7 +1794,7 @@ var init_prescribed = __esm({
1508
1794
  }
1509
1795
  async loadHierarchyConfigs() {
1510
1796
  try {
1511
- const strategies = await this.course.getNavigationStrategies();
1797
+ const strategies = await this.course.getAllNavigationStrategies();
1512
1798
  return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
1513
1799
  try {
1514
1800
  const parsed = JSON.parse(s.serializedData);
@@ -1531,6 +1817,7 @@ var init_prescribed = __esm({
1531
1817
  activeIds,
1532
1818
  seenIds,
1533
1819
  tagsByCard,
1820
+ cardsByTag,
1534
1821
  hierarchyConfigs,
1535
1822
  userTagElo,
1536
1823
  userGlobalElo
@@ -1594,6 +1881,22 @@ var init_prescribed = __esm({
1594
1881
  [...supportTags]
1595
1882
  )
1596
1883
  ]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
1884
+ const discoveredSupportCandidates = blockedTargets.length > 0 && supportTags.size > 0 && supportCandidates.length === 0 ? this.findDiscoveredSupportCards({
1885
+ supportTags: [...supportTags],
1886
+ cardsByTag,
1887
+ activeIds,
1888
+ seenIds,
1889
+ excludedIds: /* @__PURE__ */ new Set([
1890
+ ...group.targetCardIds,
1891
+ ...group.supportCardIds ?? []
1892
+ ]),
1893
+ limit: group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN
1894
+ }) : [];
1895
+ if (blockedTargets.length > 0 && supportTags.size > 0 && discoveredSupportCandidates.length === 0) {
1896
+ logger.info(
1897
+ `[Prescribed] Group '${group.id}' discovered 0 broader support candidates (blocked=${blockedTargets.length}; authoredSupport=${supportCandidates.length})`
1898
+ );
1899
+ }
1597
1900
  const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
1598
1901
  const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
1599
1902
  const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
@@ -1607,6 +1910,7 @@ var init_prescribed = __esm({
1607
1910
  surfaceableTargets,
1608
1911
  targetTags,
1609
1912
  supportCandidates,
1913
+ discoveredSupportCandidates,
1610
1914
  supportTags: [...supportTags],
1611
1915
  pressureMultiplier,
1612
1916
  supportMultiplier,
@@ -1677,6 +1981,33 @@ var init_prescribed = __esm({
1677
1981
  }
1678
1982
  return cards;
1679
1983
  }
1984
+ buildDiscoveredSupportCards(runtime, courseId, emittedIds) {
1985
+ if (runtime.blockedTargets.length === 0 || runtime.discoveredSupportCandidates.length === 0) {
1986
+ return [];
1987
+ }
1988
+ const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
1989
+ const supportIds = runtime.discoveredSupportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
1990
+ const cards = [];
1991
+ for (const cardId of supportIds) {
1992
+ emittedIds.add(cardId);
1993
+ cards.push({
1994
+ cardId,
1995
+ courseId,
1996
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
1997
+ provenance: [
1998
+ {
1999
+ strategy: "prescribed",
2000
+ strategyName: this.strategyName || this.name,
2001
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2002
+ action: "generated",
2003
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
2004
+ 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}`
2005
+ }
2006
+ ]
2007
+ });
2008
+ }
2009
+ return cards;
2010
+ }
1680
2011
  findSupportCardsByTags(group, tagsByCard, supportTags) {
1681
2012
  if (supportTags.length === 0) {
1682
2013
  return [];
@@ -1699,6 +2030,40 @@ var init_prescribed = __esm({
1699
2030
  }
1700
2031
  return [...candidates];
1701
2032
  }
2033
+ findDiscoveredSupportCards(args) {
2034
+ const { supportTags, cardsByTag, activeIds, seenIds, excludedIds, limit } = args;
2035
+ const byCardId = /* @__PURE__ */ new Map();
2036
+ for (const supportTag of supportTags) {
2037
+ const taggedCards = cardsByTag.get(supportTag) ?? [];
2038
+ for (const cardId of taggedCards) {
2039
+ if (activeIds.has(cardId) || seenIds.has(cardId) || excludedIds.has(cardId)) {
2040
+ continue;
2041
+ }
2042
+ const existing = byCardId.get(cardId);
2043
+ if (existing) {
2044
+ existing.matches += 1;
2045
+ } else {
2046
+ byCardId.set(cardId, { cardId, matches: 1 });
2047
+ }
2048
+ }
2049
+ }
2050
+ const candidates = [...byCardId.values()].sort((a, b) => b.matches - a.matches || a.cardId.localeCompare(b.cardId));
2051
+ const usedStems = /* @__PURE__ */ new Set();
2052
+ const diverse = [];
2053
+ const deferred = [];
2054
+ for (const entry of candidates) {
2055
+ const stem = extractWordStem(entry.cardId);
2056
+ if (!usedStems.has(stem)) {
2057
+ usedStems.add(stem);
2058
+ diverse.push(entry);
2059
+ } else {
2060
+ deferred.push(entry);
2061
+ }
2062
+ }
2063
+ shuffleInPlace(diverse);
2064
+ shuffleInPlace(deferred);
2065
+ return [...diverse, ...deferred].slice(0, limit).map((entry) => entry.cardId);
2066
+ }
1702
2067
  resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
1703
2068
  const supportTags = /* @__PURE__ */ new Set();
1704
2069
  let blocked = false;
@@ -1744,7 +2109,6 @@ var init_prescribed = __esm({
1744
2109
  }
1745
2110
  collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
1746
2111
  if (depth < 0 || visited.has(tag)) return;
1747
- if (this.isHardGatedTag(tag)) return;
1748
2112
  visited.add(tag);
1749
2113
  let walkedFurther = false;
1750
2114
  for (const hierarchy of hierarchyConfigs) {
@@ -1772,9 +2136,6 @@ var init_prescribed = __esm({
1772
2136
  out.add(tag);
1773
2137
  }
1774
2138
  }
1775
- isHardGatedTag(tag) {
1776
- return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
1777
- }
1778
2139
  isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
1779
2140
  if (!userTagElo) return false;
1780
2141
  const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
@@ -3320,6 +3681,32 @@ var init_Pipeline = __esm({
3320
3681
  cards = await this.hydrateTags(cards);
3321
3682
  const tHydrate = performance.now();
3322
3683
  const allCardsBeforeFiltering = [...cards];
3684
+ const pendingHints = this._ephemeralHints;
3685
+ if (pendingHints?.requireCards?.length) {
3686
+ const poolIds = new Set(allCardsBeforeFiltering.map((c) => c.cardId));
3687
+ const missingIds = pendingHints.requireCards.filter(
3688
+ (p) => !p.includes("*") && !poolIds.has(p)
3689
+ );
3690
+ if (missingIds.length > 0) {
3691
+ const fetchedTags = await this.course.getAppliedTagsBatch(missingIds);
3692
+ const courseId = this.course.getCourseID();
3693
+ for (const cardId of missingIds) {
3694
+ allCardsBeforeFiltering.push({
3695
+ cardId,
3696
+ courseId,
3697
+ score: 1,
3698
+ tags: fetchedTags.get(cardId) ?? [],
3699
+ provenance: []
3700
+ });
3701
+ }
3702
+ logger.info(
3703
+ `[Pipeline] Pre-fetched ${missingIds.length} required card(s) into pool: ${missingIds.join(", ")}`
3704
+ );
3705
+ }
3706
+ }
3707
+ const prescribedIds = new Set(
3708
+ cards.filter((c) => c.provenance.some((p) => p.strategy === "prescribed")).map((c) => c.cardId)
3709
+ );
3323
3710
  const filterImpacts = [];
3324
3711
  for (const filter of this.filters) {
3325
3712
  const beforeCount = cards.length;
@@ -3334,6 +3721,17 @@ var init_Pipeline = __esm({
3334
3721
  else passed++;
3335
3722
  }
3336
3723
  filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
3724
+ if (prescribedIds.size > 0) {
3725
+ const survivingIds = new Set(cards.map((c) => c.cardId));
3726
+ const killedPrescribed = [...prescribedIds].filter((id) => !survivingIds.has(id));
3727
+ const zeroedPrescribed = cards.filter((c) => prescribedIds.has(c.cardId) && c.score === 0).map((c) => c.cardId);
3728
+ if (killedPrescribed.length > 0 || zeroedPrescribed.length > 0) {
3729
+ logger.info(
3730
+ `[Pipeline] Filter '${filter.name}' impact on prescribed cards: ` + (killedPrescribed.length > 0 ? `removed=[${killedPrescribed.join(", ")}] ` : "") + (zeroedPrescribed.length > 0 ? `zeroed=[${zeroedPrescribed.join(", ")}]` : "")
3731
+ );
3732
+ killedPrescribed.forEach((id) => prescribedIds.delete(id));
3733
+ }
3734
+ }
3337
3735
  logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
3338
3736
  }
3339
3737
  cards = cards.filter((c) => c.score > 0);
@@ -3370,7 +3768,8 @@ var init_Pipeline = __esm({
3370
3768
  filterImpacts,
3371
3769
  cards,
3372
3770
  result,
3373
- context.userElo
3771
+ context.userElo,
3772
+ hints ?? void 0
3374
3773
  );
3375
3774
  captureRun(report);
3376
3775
  } catch (e) {
@@ -3480,13 +3879,27 @@ var init_Pipeline = __esm({
3480
3879
  }
3481
3880
  }
3482
3881
  const cardIds = new Set(cards.map((c) => c.cardId));
3882
+ const cardMap = new Map(cards.map((c) => [c.cardId, c]));
3483
3883
  const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
3484
- const inject = (card, reason) => {
3485
- if (!cardIds.has(card.cardId)) {
3486
- const floorScore = Math.max(card.score, 1);
3884
+ const applyRequirement = (card, reason) => {
3885
+ const mandatoryScore = Number.POSITIVE_INFINITY;
3886
+ const existing = cardMap.get(card.cardId);
3887
+ if (existing) {
3888
+ if (existing.score < mandatoryScore) {
3889
+ existing.score = mandatoryScore;
3890
+ existing.provenance.push({
3891
+ strategy: "ephemeralHint",
3892
+ strategyId: "ephemeral-hint",
3893
+ strategyName: hintLabel,
3894
+ action: "boosted",
3895
+ score: mandatoryScore,
3896
+ reason: `${reason} (upgrade to mandatory score)`
3897
+ });
3898
+ }
3899
+ } else {
3487
3900
  cards.push({
3488
3901
  ...card,
3489
- score: floorScore,
3902
+ score: mandatoryScore,
3490
3903
  provenance: [
3491
3904
  ...card.provenance,
3492
3905
  {
@@ -3494,25 +3907,41 @@ var init_Pipeline = __esm({
3494
3907
  strategyId: "ephemeral-hint",
3495
3908
  strategyName: hintLabel,
3496
3909
  action: "boosted",
3497
- score: floorScore,
3910
+ score: mandatoryScore,
3498
3911
  reason
3499
3912
  }
3500
3913
  ]
3501
3914
  });
3502
3915
  cardIds.add(card.cardId);
3916
+ cardMap.set(card.cardId, cards[cards.length - 1]);
3503
3917
  }
3504
3918
  };
3505
3919
  if (hints.requireCards?.length) {
3506
3920
  for (const pattern of hints.requireCards) {
3921
+ for (const cardId of cardIds) {
3922
+ if (globMatch(cardId, pattern)) {
3923
+ applyRequirement(cardMap.get(cardId), `requireCard ${pattern}`);
3924
+ }
3925
+ }
3507
3926
  for (const card of allCards) {
3508
- if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
3927
+ if (globMatch(card.cardId, pattern)) {
3928
+ applyRequirement(card, `requireCard ${pattern}`);
3929
+ }
3509
3930
  }
3510
3931
  }
3511
3932
  }
3512
3933
  if (hints.requireTags?.length) {
3513
3934
  for (const pattern of hints.requireTags) {
3935
+ for (const cardId of cardIds) {
3936
+ const card = cardMap.get(cardId);
3937
+ if (cardMatchesTagPattern(card, pattern)) {
3938
+ applyRequirement(card, `requireTag ${pattern}`);
3939
+ }
3940
+ }
3514
3941
  for (const card of allCards) {
3515
- if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
3942
+ if (cardMatchesTagPattern(card, pattern)) {
3943
+ applyRequirement(card, `requireTag ${pattern}`);
3944
+ }
3516
3945
  }
3517
3946
  }
3518
3947
  }