@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.
@@ -514,8 +514,9 @@ function getOrigin(card) {
514
514
  const firstEntry = card.provenance[0];
515
515
  if (!firstEntry) return "unknown";
516
516
  const reason = firstEntry.reason?.toLowerCase() || "";
517
- if (reason.includes("new card")) return "new";
518
- if (reason.includes("review")) return "review";
517
+ const strategy = firstEntry.strategy?.toLowerCase() || "";
518
+ if (reason.includes("new card") || strategy.includes("elo")) return "new";
519
+ if (reason.includes("review") || strategy.includes("srs")) return "review";
519
520
  return "unknown";
520
521
  }
521
522
  function captureRun(report) {
@@ -535,12 +536,13 @@ function parseCardElo(provenance) {
535
536
  const match = eloEntry.reason.match(/card:\s*(\d+)/);
536
537
  return match ? parseInt(match[1], 10) : void 0;
537
538
  }
538
- function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo) {
539
+ function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
539
540
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
540
541
  const cards = allCards.map((card) => ({
541
542
  cardId: card.cardId,
542
543
  courseId: card.courseId,
543
544
  origin: getOrigin(card),
545
+ generator: card.provenance[0]?.strategyName || card.provenance[0]?.strategy,
544
546
  finalScore: card.score,
545
547
  cardElo: parseCardElo(card.provenance),
546
548
  provenance: card.provenance,
@@ -557,6 +559,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
557
559
  generators,
558
560
  generatedCount,
559
561
  filters,
562
+ hints,
560
563
  finalCount: selectedCards.length,
561
564
  reviewsSelected,
562
565
  newSelected,
@@ -596,13 +599,169 @@ function printRunSummary(run) {
596
599
  );
597
600
  console.groupEnd();
598
601
  }
602
+ function renderUI() {
603
+ if (!_uiContainer) return;
604
+ const runs = runHistory;
605
+ const selectedRun = _selectedRunIndex !== null ? runs[_selectedRunIndex] : null;
606
+ const styles = `
607
+ #sk-pipeline-debugger {
608
+ position: fixed;
609
+ top: 0;
610
+ left: 0;
611
+ width: 100vw;
612
+ height: 100vh;
613
+ background: #f8f9fa;
614
+ color: #212529;
615
+ z-index: 999999;
616
+ display: flex;
617
+ flex-direction: column;
618
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
619
+ font-size: 14px;
620
+ }
621
+ #sk-pipeline-debugger header {
622
+ padding: 1rem;
623
+ background: #343a40;
624
+ color: white;
625
+ display: flex;
626
+ justify-content: space-between;
627
+ align-items: center;
628
+ }
629
+ #sk-pipeline-debugger .container {
630
+ display: flex;
631
+ flex: 1;
632
+ overflow: hidden;
633
+ }
634
+ #sk-pipeline-debugger .sidebar {
635
+ width: 300px;
636
+ border-right: 1px solid #dee2e6;
637
+ overflow-y: auto;
638
+ background: white;
639
+ }
640
+ #sk-pipeline-debugger .main-content {
641
+ flex: 1;
642
+ overflow-y: auto;
643
+ padding: 1.5rem;
644
+ }
645
+ #sk-pipeline-debugger .run-item {
646
+ padding: 0.75rem 1rem;
647
+ border-bottom: 1px solid #eee;
648
+ cursor: pointer;
649
+ }
650
+ #sk-pipeline-debugger .run-item:hover { background: #f1f3f5; }
651
+ #sk-pipeline-debugger .run-item.active { background: #e9ecef; border-left: 4px solid #007bff; }
652
+ #sk-pipeline-debugger h2, #sk-pipeline-debugger h3 { margin-top: 0; }
653
+ #sk-pipeline-debugger table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; background: white; }
654
+ #sk-pipeline-debugger th, #sk-pipeline-debugger td { border: 1px solid #dee2e6; padding: 0.5rem; text-align: left; }
655
+ #sk-pipeline-debugger th { background: #f1f3f5; }
656
+ #sk-pipeline-debugger code { background: #f1f3f5; padding: 0.1rem 0.3rem; border-radius: 3px; font-family: monospace; }
657
+ #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
658
+ #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
659
+ #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; }
660
+ `;
661
+ const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
662
+ (r, i) => `
663
+ <div class="run-item ${i === _selectedRunIndex ? "active" : ""}" onclick="window.skuilder.pipeline._selectRun(${i})">
664
+ <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
665
+ <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
666
+ <small>${r.finalCount} cards selected</small>
667
+ </div>
668
+ `
669
+ ).join("");
670
+ let detailsHtml = '<div style="color: #6c757d; text-align: center; margin-top: 5rem;">Select a run to see details</div>';
671
+ if (selectedRun) {
672
+ const filteredCards = selectedRun.cards.filter(
673
+ (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
674
+ );
675
+ detailsHtml = `
676
+ <h2>Run: ${selectedRun.runId}</h2>
677
+ <p>
678
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
679
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
680
+ <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
681
+ </p>
682
+
683
+ <h3>Pipeline Config</h3>
684
+ <table>
685
+ <tr><th>Generator</th><td>${selectedRun.generatorName} (${selectedRun.generatedCount} candidates)</td></tr>
686
+ ${(selectedRun.generators || []).map(
687
+ (g) => `
688
+ <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>
689
+ `
690
+ ).join("")}
691
+ </table>
692
+
693
+ ${selectedRun.hints ? `
694
+ <h3>Ephemeral Hints</h3>
695
+ <table>
696
+ ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
697
+ ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
698
+ ${selectedRun.hints.boostCards ? `<tr><th>Boost Cards</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostCards, null, 2)}</pre></td></tr>` : ""}
699
+ ${selectedRun.hints.requireTags ? `<tr><th>Require Tags</th><td>${selectedRun.hints.requireTags.join(", ")}</td></tr>` : ""}
700
+ ${selectedRun.hints.requireCards ? `<tr><th>Require Cards</th><td>${selectedRun.hints.requireCards.join(", ")}</td></tr>` : ""}
701
+ ${selectedRun.hints.excludeTags ? `<tr><th>Exclude Tags</th><td>${selectedRun.hints.excludeTags.join(", ")}</td></tr>` : ""}
702
+ ${selectedRun.hints.excludeCards ? `<tr><th>Exclude Cards</th><td>${selectedRun.hints.excludeCards.join(", ")}</td></tr>` : ""}
703
+ </table>
704
+ ` : ""}
705
+
706
+ <h3>Filter Impact</h3>
707
+ <table>
708
+ <thead><tr><th>Filter</th><th>Boosted</th><th>Penalized</th><th>Passed</th><th>Removed</th></tr></thead>
709
+ <tbody>
710
+ ${selectedRun.filters.map(
711
+ (f) => `
712
+ <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>
713
+ `
714
+ ).join("")}
715
+ </tbody>
716
+ </table>
717
+
718
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
719
+ <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
720
+
721
+ <table>
722
+ <thead><tr><th>ID</th><th>Generator</th><th>Origin</th><th>Score</th><th>Selected</th></tr></thead>
723
+ <tbody>
724
+ ${filteredCards.map(
725
+ (c) => `
726
+ <tr>
727
+ <td><code>${c.cardId}</code></td>
728
+ <td>${c.generator || "unknown"}</td>
729
+ <td>${c.origin}</td>
730
+ <td>${c.finalScore.toFixed(3)}</td>
731
+ <td>${c.selected ? "\u2705" : "\u274C"}</td>
732
+ </tr>
733
+ ${c.selected || _cardSearchQuery ? `
734
+ <tr>
735
+ <td colspan="5">
736
+ <div class="provenance">${formatProvenance(c.provenance)}</div>
737
+ </td>
738
+ </tr>
739
+ ` : ""}
740
+ `
741
+ ).join("")}
742
+ </tbody>
743
+ </table>
744
+ `;
745
+ }
746
+ _uiContainer.innerHTML = `
747
+ <style>${styles}</style>
748
+ <header>
749
+ <strong>Pipeline Debugger</strong>
750
+ <button class="close-btn" onclick="window.skuilder.pipeline.ui()">Close</button>
751
+ </header>
752
+ <div class="container">
753
+ <div class="sidebar">${runListHtml}</div>
754
+ <div class="main-content">${detailsHtml}</div>
755
+ </div>
756
+ `;
757
+ }
599
758
  function mountPipelineDebugger() {
600
759
  if (typeof window === "undefined") return;
601
760
  const win = window;
602
761
  win.skuilder = win.skuilder || {};
603
762
  win.skuilder.pipeline = pipelineDebugAPI;
604
763
  }
605
- var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
764
+ var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
606
765
  var init_PipelineDebugger = __esm({
607
766
  "src/core/navigators/PipelineDebugger.ts"() {
608
767
  "use strict";
@@ -611,6 +770,9 @@ var init_PipelineDebugger = __esm({
611
770
  _activePipeline = null;
612
771
  MAX_RUNS = 10;
613
772
  runHistory = [];
773
+ _uiContainer = null;
774
+ _selectedRunIndex = null;
775
+ _cardSearchQuery = "";
614
776
  pipelineDebugAPI = {
615
777
  /**
616
778
  * Get raw run history for programmatic access.
@@ -750,16 +912,20 @@ var init_PipelineDebugger = __esm({
750
912
  const mode = reason.match(/mode=([^;]+)/)?.[1] ?? "unknown";
751
913
  const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? "unknown";
752
914
  const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? "none";
915
+ const supportCard = reason.match(/supportCard=([^;]+)/)?.[1] ?? "none";
753
916
  const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? "none";
754
917
  const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? "unknown";
918
+ const supportSource = mode === "discovered-support" ? "discovered" : mode === "support" ? "authored" : "n/a";
755
919
  return {
756
920
  group: parsedGroup,
757
921
  mode,
922
+ supportSource,
758
923
  cardId: card.cardId,
759
924
  selected: card.selected ? "yes" : "no",
760
925
  finalScore: card.finalScore.toFixed(3),
761
926
  blocked,
762
927
  blockedTargets,
928
+ supportCard,
763
929
  supportTags,
764
930
  multiplier
765
931
  };
@@ -775,6 +941,8 @@ var init_PipelineDebugger = __esm({
775
941
  const selectedRows = rows.filter((r) => r.selected === "yes");
776
942
  const blockedTargetSet = /* @__PURE__ */ new Set();
777
943
  const supportTagSet = /* @__PURE__ */ new Set();
944
+ const authoredSupportSet = /* @__PURE__ */ new Set();
945
+ const discoveredSupportSet = /* @__PURE__ */ new Set();
778
946
  for (const row of rows) {
779
947
  if (row.blockedTargets && row.blockedTargets !== "none") {
780
948
  row.blockedTargets.split("|").filter(Boolean).forEach((t) => blockedTargetSet.add(t));
@@ -782,6 +950,13 @@ var init_PipelineDebugger = __esm({
782
950
  if (row.supportTags && row.supportTags !== "none") {
783
951
  row.supportTags.split("|").filter(Boolean).forEach((t) => supportTagSet.add(t));
784
952
  }
953
+ if (row.supportCard && row.supportCard !== "none") {
954
+ if (row.supportSource === "discovered") {
955
+ discoveredSupportSet.add(row.supportCard);
956
+ } else if (row.supportSource === "authored") {
957
+ authoredSupportSet.add(row.supportCard);
958
+ }
959
+ }
785
960
  }
786
961
  logger.info(`Prescribed cards in run: ${rows.length}`);
787
962
  logger.info(`Selected prescribed cards: ${selectedRows.length}`);
@@ -791,6 +966,12 @@ var init_PipelineDebugger = __esm({
791
966
  logger.info(
792
967
  `Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(", ") : "none"}`
793
968
  );
969
+ logger.info(
970
+ `Authored support cards emitted: ${authoredSupportSet.size > 0 ? [...authoredSupportSet].join(", ") : "none"}`
971
+ );
972
+ logger.info(
973
+ `Discovered support cards emitted: ${discoveredSupportSet.size > 0 ? [...discoveredSupportSet].join(", ") : "none"}`
974
+ );
794
975
  console.groupEnd();
795
976
  },
796
977
  /**
@@ -925,6 +1106,39 @@ var init_PipelineDebugger = __esm({
925
1106
  Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
926
1107
  );
927
1108
  },
1109
+ /**
1110
+ * Toggle the full-screen UI debugger.
1111
+ */
1112
+ ui() {
1113
+ if (_uiContainer) {
1114
+ document.body.removeChild(_uiContainer);
1115
+ _uiContainer = null;
1116
+ return;
1117
+ }
1118
+ _uiContainer = document.createElement("div");
1119
+ _uiContainer.id = "sk-pipeline-debugger";
1120
+ document.body.appendChild(_uiContainer);
1121
+ if (_selectedRunIndex === null && runHistory.length > 0) {
1122
+ _selectedRunIndex = 0;
1123
+ }
1124
+ renderUI();
1125
+ },
1126
+ /**
1127
+ * Internal UI helpers
1128
+ * @internal
1129
+ */
1130
+ _selectRun(index) {
1131
+ _selectedRunIndex = index;
1132
+ renderUI();
1133
+ },
1134
+ /**
1135
+ * Internal UI helpers
1136
+ * @internal
1137
+ */
1138
+ _setSearch(query) {
1139
+ _cardSearchQuery = query;
1140
+ renderUI();
1141
+ },
928
1142
  /**
929
1143
  * Show help.
930
1144
  */
@@ -933,6 +1147,7 @@ var init_PipelineDebugger = __esm({
933
1147
  \u{1F527} Pipeline Debug API
934
1148
 
935
1149
  Commands:
1150
+ .ui() Toggle full-screen UI debugger
936
1151
  .showLastRun() Show summary of most recent pipeline run
937
1152
  .showRun(id|index) Show summary of a specific run (by index or ID suffix)
938
1153
  .showCard(cardId) Show provenance trail for a specific card
@@ -949,6 +1164,7 @@ Commands:
949
1164
  .help() Show this help message
950
1165
 
951
1166
  Example:
1167
+ window.skuilder.pipeline.ui()
952
1168
  window.skuilder.pipeline.showLastRun()
953
1169
  window.skuilder.pipeline.showRun(1)
954
1170
  await window.skuilder.pipeline.diagnoseCardSpace()
@@ -1111,7 +1327,7 @@ var init_CompositeGenerator = __esm({
1111
1327
  for (const [, items] of byCardId) {
1112
1328
  const cards2 = items.map((i) => i.card);
1113
1329
  const aggregatedScore = this.aggregateScores(items);
1114
- const finalScore = Math.min(1, aggregatedScore);
1330
+ const finalScore = Math.max(0, aggregatedScore);
1115
1331
  const mergedProvenance = cards2.flatMap((c) => c.provenance);
1116
1332
  const initialScore = cards2[0].score;
1117
1333
  const action = finalScore > initialScore ? "boosted" : finalScore < initialScore ? "penalized" : "passed";
@@ -1308,10 +1524,26 @@ function matchesTagPattern(tag, pattern) {
1308
1524
  const re = new RegExp(`^${escaped}$`);
1309
1525
  return re.test(tag);
1310
1526
  }
1527
+ function extractWordStem(cardId) {
1528
+ for (const prefix of ["c-ml-", "c-ws-", "c-spelling-"]) {
1529
+ if (cardId.startsWith(prefix)) {
1530
+ const rest = cardId.slice(prefix.length);
1531
+ const lastDash = rest.lastIndexOf("-");
1532
+ return lastDash > 0 ? rest.slice(0, lastDash) : rest;
1533
+ }
1534
+ }
1535
+ return cardId;
1536
+ }
1537
+ function shuffleInPlace(arr) {
1538
+ for (let i = arr.length - 1; i > 0; i--) {
1539
+ const j = Math.floor(Math.random() * (i + 1));
1540
+ [arr[i], arr[j]] = [arr[j], arr[i]];
1541
+ }
1542
+ }
1311
1543
  function pickTopByScore(cards, limit) {
1312
1544
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1313
1545
  }
1314
- 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;
1546
+ 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;
1315
1547
  var init_prescribed = __esm({
1316
1548
  "src/core/navigators/generators/prescribed.ts"() {
1317
1549
  "use strict";
@@ -1324,11 +1556,10 @@ var init_prescribed = __esm({
1324
1556
  DEFAULT_MIN_COUNT = 3;
1325
1557
  BASE_TARGET_SCORE = 1;
1326
1558
  BASE_SUPPORT_SCORE = 0.8;
1559
+ DISCOVERED_SUPPORT_SCORE = 12;
1327
1560
  MAX_TARGET_MULTIPLIER = 8;
1328
1561
  MAX_SUPPORT_MULTIPLIER = 4;
1329
- LOCKED_TAG_PREFIXES = ["concept:"];
1330
- LESSON_GATE_PENALTY_TAG_HINT = "concept:";
1331
- PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v2";
1562
+ PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
1332
1563
  PrescribedCardsGenerator = class extends ContentNavigator {
1333
1564
  name;
1334
1565
  config;
@@ -1364,6 +1595,20 @@ var init_prescribed = __esm({
1364
1595
  const allSupportIds = dedupe(this.config.groups.flatMap((g) => g.supportCardIds ?? []));
1365
1596
  const allRelevantIds = dedupe([...allTargetIds, ...allSupportIds]);
1366
1597
  const tagsByCard = allRelevantIds.length > 0 ? await this.course.getAppliedTagsBatch(allRelevantIds) : /* @__PURE__ */ new Map();
1598
+ const courseTagDocs = await this.course.getCourseTagStubs().catch(
1599
+ () => ({
1600
+ rows: [],
1601
+ offset: 0,
1602
+ total_rows: 0
1603
+ })
1604
+ );
1605
+ const cardsByTag = /* @__PURE__ */ new Map();
1606
+ for (const row of courseTagDocs.rows ?? []) {
1607
+ const tagDoc = row.doc;
1608
+ if (tagDoc?.name && Array.isArray(tagDoc.taggedCards)) {
1609
+ cardsByTag.set(tagDoc.name, [...tagDoc.taggedCards]);
1610
+ }
1611
+ }
1367
1612
  const nextState = {
1368
1613
  updatedAt: isoNow(),
1369
1614
  groups: {}
@@ -1378,11 +1623,31 @@ var init_prescribed = __esm({
1378
1623
  activeIds,
1379
1624
  seenIds,
1380
1625
  tagsByCard,
1626
+ cardsByTag,
1381
1627
  hierarchyConfigs,
1382
1628
  userTagElo,
1383
1629
  userGlobalElo
1384
1630
  });
1385
1631
  groupRuntimes.push(runtime);
1632
+ logger.info(
1633
+ `[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)}`
1634
+ );
1635
+ if (runtime.blockedTargets.length > 0) {
1636
+ logger.info(
1637
+ `[Prescribed] Group '${group.id}' blocked targets: ${runtime.blockedTargets.join(", ")}`
1638
+ );
1639
+ logger.info(
1640
+ `[Prescribed] Group '${group.id}' support tags needed: ${runtime.supportTags.join(", ") || "(none)"}`
1641
+ );
1642
+ logger.info(
1643
+ `[Prescribed] Group '${group.id}' escalation mode: ` + (runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support-candidates" : "boost-only")
1644
+ );
1645
+ if (runtime.discoveredSupportCandidates.length > 0) {
1646
+ logger.info(
1647
+ `[Prescribed] Group '${group.id}' discovered support candidates: ${runtime.discoveredSupportCandidates.join(", ")}`
1648
+ );
1649
+ }
1650
+ }
1386
1651
  nextState.groups[group.id] = this.buildNextGroupState(runtime, progress.groups[group.id]);
1387
1652
  const directCards = this.buildDirectTargetCards(
1388
1653
  runtime,
@@ -1394,15 +1659,30 @@ var init_prescribed = __esm({
1394
1659
  courseId,
1395
1660
  emittedIds
1396
1661
  );
1397
- emitted.push(...directCards, ...supportCards);
1662
+ const discoveredSupportCards = this.buildDiscoveredSupportCards(
1663
+ runtime,
1664
+ courseId,
1665
+ emittedIds
1666
+ );
1667
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
1398
1668
  }
1399
1669
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1400
1670
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
1401
1671
  boostTags: hintSummary.boostTags,
1402
1672
  _label: `prescribed-support (${hintSummary.supportTags.length} tags; blocked=${hintSummary.blockedTargetIds.length}; testversion=${PRESCRIBED_DEBUG_VERSION})`
1403
1673
  } : void 0;
1674
+ if (hints) {
1675
+ const tagEntries = Object.entries(hints.boostTags ?? {});
1676
+ logger.info(
1677
+ `[Prescribed] Emitting ${tagEntries.length} boost hint(s): ` + tagEntries.map(([tag, mult]) => `${tag}\xD7${mult.toFixed(1)}`).join(", ")
1678
+ );
1679
+ } else {
1680
+ logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
1681
+ }
1404
1682
  if (emitted.length === 0) {
1405
- logger.debug("[Prescribed] No prescribed targets/support emitted this run");
1683
+ logger.info(
1684
+ "[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
1685
+ );
1406
1686
  await this.putStrategyState(nextState).catch((e) => {
1407
1687
  logger.debug(`[Prescribed] Failed to persist empty-state update: ${e}`);
1408
1688
  });
@@ -1435,7 +1715,7 @@ var init_prescribed = __esm({
1435
1715
  logger.debug(`[Prescribed] Failed to persist prescribed progress: ${e}`);
1436
1716
  });
1437
1717
  logger.info(
1438
- `[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)`
1718
+ `[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)`
1439
1719
  );
1440
1720
  return hints ? { cards: finalCards, hints } : { cards: finalCards };
1441
1721
  }
@@ -1465,9 +1745,15 @@ var init_prescribed = __esm({
1465
1745
  const groupsRaw = Array.isArray(parsed.groups) ? parsed.groups : [];
1466
1746
  const groups = groupsRaw.map((raw, i) => ({
1467
1747
  id: typeof raw.id === "string" && raw.id.trim().length > 0 ? raw.id : `group-${i + 1}`,
1468
- targetCardIds: dedupe(Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []),
1469
- supportCardIds: dedupe(Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []),
1470
- supportTagPatterns: dedupe(Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []),
1748
+ targetCardIds: dedupe(
1749
+ Array.isArray(raw.targetCardIds) ? raw.targetCardIds.filter((v) => typeof v === "string") : []
1750
+ ),
1751
+ supportCardIds: dedupe(
1752
+ Array.isArray(raw.supportCardIds) ? raw.supportCardIds.filter((v) => typeof v === "string") : []
1753
+ ),
1754
+ supportTagPatterns: dedupe(
1755
+ Array.isArray(raw.supportTagPatterns) ? raw.supportTagPatterns.filter((v) => typeof v === "string") : []
1756
+ ),
1471
1757
  freshnessWindowSessions: typeof raw.freshnessWindowSessions === "number" ? raw.freshnessWindowSessions : DEFAULT_FRESHNESS_WINDOW,
1472
1758
  maxDirectTargetsPerRun: typeof raw.maxDirectTargetsPerRun === "number" ? raw.maxDirectTargetsPerRun : DEFAULT_MAX_DIRECT_PER_RUN,
1473
1759
  maxSupportCardsPerRun: typeof raw.maxSupportCardsPerRun === "number" ? raw.maxSupportCardsPerRun : DEFAULT_MAX_SUPPORT_PER_RUN,
@@ -1484,7 +1770,7 @@ var init_prescribed = __esm({
1484
1770
  }
1485
1771
  async loadHierarchyConfigs() {
1486
1772
  try {
1487
- const strategies = await this.course.getNavigationStrategies();
1773
+ const strategies = await this.course.getAllNavigationStrategies();
1488
1774
  return strategies.filter((s) => s.implementingClass === "hierarchyDefinition").map((s) => {
1489
1775
  try {
1490
1776
  const parsed = JSON.parse(s.serializedData);
@@ -1507,6 +1793,7 @@ var init_prescribed = __esm({
1507
1793
  activeIds,
1508
1794
  seenIds,
1509
1795
  tagsByCard,
1796
+ cardsByTag,
1510
1797
  hierarchyConfigs,
1511
1798
  userTagElo,
1512
1799
  userGlobalElo
@@ -1570,6 +1857,22 @@ var init_prescribed = __esm({
1570
1857
  [...supportTags]
1571
1858
  )
1572
1859
  ]).filter((id) => !activeIds.has(id) && !seenIds.has(id));
1860
+ const discoveredSupportCandidates = blockedTargets.length > 0 && supportTags.size > 0 && supportCandidates.length === 0 ? this.findDiscoveredSupportCards({
1861
+ supportTags: [...supportTags],
1862
+ cardsByTag,
1863
+ activeIds,
1864
+ seenIds,
1865
+ excludedIds: /* @__PURE__ */ new Set([
1866
+ ...group.targetCardIds,
1867
+ ...group.supportCardIds ?? []
1868
+ ]),
1869
+ limit: group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN
1870
+ }) : [];
1871
+ if (blockedTargets.length > 0 && supportTags.size > 0 && discoveredSupportCandidates.length === 0) {
1872
+ logger.info(
1873
+ `[Prescribed] Group '${group.id}' discovered 0 broader support candidates (blocked=${blockedTargets.length}; authoredSupport=${supportCandidates.length})`
1874
+ );
1875
+ }
1573
1876
  const sessionsSinceSurfaced = priorState?.sessionsSinceSurfaced ?? 0;
1574
1877
  const freshnessWindow = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
1575
1878
  const staleSessions = Math.max(0, sessionsSinceSurfaced - freshnessWindow);
@@ -1583,6 +1886,7 @@ var init_prescribed = __esm({
1583
1886
  surfaceableTargets,
1584
1887
  targetTags,
1585
1888
  supportCandidates,
1889
+ discoveredSupportCandidates,
1586
1890
  supportTags: [...supportTags],
1587
1891
  pressureMultiplier,
1588
1892
  supportMultiplier,
@@ -1653,6 +1957,33 @@ var init_prescribed = __esm({
1653
1957
  }
1654
1958
  return cards;
1655
1959
  }
1960
+ buildDiscoveredSupportCards(runtime, courseId, emittedIds) {
1961
+ if (runtime.blockedTargets.length === 0 || runtime.discoveredSupportCandidates.length === 0) {
1962
+ return [];
1963
+ }
1964
+ const maxSupport = runtime.group.maxSupportCardsPerRun ?? DEFAULT_MAX_SUPPORT_PER_RUN;
1965
+ const supportIds = runtime.discoveredSupportCandidates.filter((id) => !emittedIds.has(id)).slice(0, maxSupport);
1966
+ const cards = [];
1967
+ for (const cardId of supportIds) {
1968
+ emittedIds.add(cardId);
1969
+ cards.push({
1970
+ cardId,
1971
+ courseId,
1972
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
1973
+ provenance: [
1974
+ {
1975
+ strategy: "prescribed",
1976
+ strategyName: this.strategyName || this.name,
1977
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
1978
+ action: "generated",
1979
+ score: DISCOVERED_SUPPORT_SCORE * runtime.supportMultiplier,
1980
+ 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}`
1981
+ }
1982
+ ]
1983
+ });
1984
+ }
1985
+ return cards;
1986
+ }
1656
1987
  findSupportCardsByTags(group, tagsByCard, supportTags) {
1657
1988
  if (supportTags.length === 0) {
1658
1989
  return [];
@@ -1675,6 +2006,40 @@ var init_prescribed = __esm({
1675
2006
  }
1676
2007
  return [...candidates];
1677
2008
  }
2009
+ findDiscoveredSupportCards(args) {
2010
+ const { supportTags, cardsByTag, activeIds, seenIds, excludedIds, limit } = args;
2011
+ const byCardId = /* @__PURE__ */ new Map();
2012
+ for (const supportTag of supportTags) {
2013
+ const taggedCards = cardsByTag.get(supportTag) ?? [];
2014
+ for (const cardId of taggedCards) {
2015
+ if (activeIds.has(cardId) || seenIds.has(cardId) || excludedIds.has(cardId)) {
2016
+ continue;
2017
+ }
2018
+ const existing = byCardId.get(cardId);
2019
+ if (existing) {
2020
+ existing.matches += 1;
2021
+ } else {
2022
+ byCardId.set(cardId, { cardId, matches: 1 });
2023
+ }
2024
+ }
2025
+ }
2026
+ const candidates = [...byCardId.values()].sort((a, b) => b.matches - a.matches || a.cardId.localeCompare(b.cardId));
2027
+ const usedStems = /* @__PURE__ */ new Set();
2028
+ const diverse = [];
2029
+ const deferred = [];
2030
+ for (const entry of candidates) {
2031
+ const stem = extractWordStem(entry.cardId);
2032
+ if (!usedStems.has(stem)) {
2033
+ usedStems.add(stem);
2034
+ diverse.push(entry);
2035
+ } else {
2036
+ deferred.push(entry);
2037
+ }
2038
+ }
2039
+ shuffleInPlace(diverse);
2040
+ shuffleInPlace(deferred);
2041
+ return [...diverse, ...deferred].slice(0, limit).map((entry) => entry.cardId);
2042
+ }
1678
2043
  resolveBlockedSupportTags(targetTags, hierarchyConfigs, userTagElo, userGlobalElo, hierarchyWalkEnabled, maxDepth) {
1679
2044
  const supportTags = /* @__PURE__ */ new Set();
1680
2045
  let blocked = false;
@@ -1720,7 +2085,6 @@ var init_prescribed = __esm({
1720
2085
  }
1721
2086
  collectSupportTagsRecursive(tag, hierarchyConfigs, userTagElo, userGlobalElo, depth, visited, out) {
1722
2087
  if (depth < 0 || visited.has(tag)) return;
1723
- if (this.isHardGatedTag(tag)) return;
1724
2088
  visited.add(tag);
1725
2089
  let walkedFurther = false;
1726
2090
  for (const hierarchy of hierarchyConfigs) {
@@ -1748,9 +2112,6 @@ var init_prescribed = __esm({
1748
2112
  out.add(tag);
1749
2113
  }
1750
2114
  }
1751
- isHardGatedTag(tag) {
1752
- return LOCKED_TAG_PREFIXES.some((prefix) => tag.startsWith(prefix)) && tag.startsWith(LESSON_GATE_PENALTY_TAG_HINT);
1753
- }
1754
2115
  isPrerequisiteMet(prereq, userTagElo, userGlobalElo) {
1755
2116
  if (!userTagElo) return false;
1756
2117
  const minCount = prereq.masteryThreshold?.minCount ?? DEFAULT_MIN_COUNT;
@@ -3296,6 +3657,32 @@ var init_Pipeline = __esm({
3296
3657
  cards = await this.hydrateTags(cards);
3297
3658
  const tHydrate = performance.now();
3298
3659
  const allCardsBeforeFiltering = [...cards];
3660
+ const pendingHints = this._ephemeralHints;
3661
+ if (pendingHints?.requireCards?.length) {
3662
+ const poolIds = new Set(allCardsBeforeFiltering.map((c) => c.cardId));
3663
+ const missingIds = pendingHints.requireCards.filter(
3664
+ (p) => !p.includes("*") && !poolIds.has(p)
3665
+ );
3666
+ if (missingIds.length > 0) {
3667
+ const fetchedTags = await this.course.getAppliedTagsBatch(missingIds);
3668
+ const courseId = this.course.getCourseID();
3669
+ for (const cardId of missingIds) {
3670
+ allCardsBeforeFiltering.push({
3671
+ cardId,
3672
+ courseId,
3673
+ score: 1,
3674
+ tags: fetchedTags.get(cardId) ?? [],
3675
+ provenance: []
3676
+ });
3677
+ }
3678
+ logger.info(
3679
+ `[Pipeline] Pre-fetched ${missingIds.length} required card(s) into pool: ${missingIds.join(", ")}`
3680
+ );
3681
+ }
3682
+ }
3683
+ const prescribedIds = new Set(
3684
+ cards.filter((c) => c.provenance.some((p) => p.strategy === "prescribed")).map((c) => c.cardId)
3685
+ );
3299
3686
  const filterImpacts = [];
3300
3687
  for (const filter of this.filters) {
3301
3688
  const beforeCount = cards.length;
@@ -3310,6 +3697,17 @@ var init_Pipeline = __esm({
3310
3697
  else passed++;
3311
3698
  }
3312
3699
  filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
3700
+ if (prescribedIds.size > 0) {
3701
+ const survivingIds = new Set(cards.map((c) => c.cardId));
3702
+ const killedPrescribed = [...prescribedIds].filter((id) => !survivingIds.has(id));
3703
+ const zeroedPrescribed = cards.filter((c) => prescribedIds.has(c.cardId) && c.score === 0).map((c) => c.cardId);
3704
+ if (killedPrescribed.length > 0 || zeroedPrescribed.length > 0) {
3705
+ logger.info(
3706
+ `[Pipeline] Filter '${filter.name}' impact on prescribed cards: ` + (killedPrescribed.length > 0 ? `removed=[${killedPrescribed.join(", ")}] ` : "") + (zeroedPrescribed.length > 0 ? `zeroed=[${zeroedPrescribed.join(", ")}]` : "")
3707
+ );
3708
+ killedPrescribed.forEach((id) => prescribedIds.delete(id));
3709
+ }
3710
+ }
3313
3711
  logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
3314
3712
  }
3315
3713
  cards = cards.filter((c) => c.score > 0);
@@ -3346,7 +3744,8 @@ var init_Pipeline = __esm({
3346
3744
  filterImpacts,
3347
3745
  cards,
3348
3746
  result,
3349
- context.userElo
3747
+ context.userElo,
3748
+ hints ?? void 0
3350
3749
  );
3351
3750
  captureRun(report);
3352
3751
  } catch (e) {
@@ -3456,13 +3855,27 @@ var init_Pipeline = __esm({
3456
3855
  }
3457
3856
  }
3458
3857
  const cardIds = new Set(cards.map((c) => c.cardId));
3858
+ const cardMap = new Map(cards.map((c) => [c.cardId, c]));
3459
3859
  const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
3460
- const inject = (card, reason) => {
3461
- if (!cardIds.has(card.cardId)) {
3462
- const floorScore = Math.max(card.score, 1);
3860
+ const applyRequirement = (card, reason) => {
3861
+ const mandatoryScore = Number.POSITIVE_INFINITY;
3862
+ const existing = cardMap.get(card.cardId);
3863
+ if (existing) {
3864
+ if (existing.score < mandatoryScore) {
3865
+ existing.score = mandatoryScore;
3866
+ existing.provenance.push({
3867
+ strategy: "ephemeralHint",
3868
+ strategyId: "ephemeral-hint",
3869
+ strategyName: hintLabel,
3870
+ action: "boosted",
3871
+ score: mandatoryScore,
3872
+ reason: `${reason} (upgrade to mandatory score)`
3873
+ });
3874
+ }
3875
+ } else {
3463
3876
  cards.push({
3464
3877
  ...card,
3465
- score: floorScore,
3878
+ score: mandatoryScore,
3466
3879
  provenance: [
3467
3880
  ...card.provenance,
3468
3881
  {
@@ -3470,25 +3883,41 @@ var init_Pipeline = __esm({
3470
3883
  strategyId: "ephemeral-hint",
3471
3884
  strategyName: hintLabel,
3472
3885
  action: "boosted",
3473
- score: floorScore,
3886
+ score: mandatoryScore,
3474
3887
  reason
3475
3888
  }
3476
3889
  ]
3477
3890
  });
3478
3891
  cardIds.add(card.cardId);
3892
+ cardMap.set(card.cardId, cards[cards.length - 1]);
3479
3893
  }
3480
3894
  };
3481
3895
  if (hints.requireCards?.length) {
3482
3896
  for (const pattern of hints.requireCards) {
3897
+ for (const cardId of cardIds) {
3898
+ if (globMatch(cardId, pattern)) {
3899
+ applyRequirement(cardMap.get(cardId), `requireCard ${pattern}`);
3900
+ }
3901
+ }
3483
3902
  for (const card of allCards) {
3484
- if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
3903
+ if (globMatch(card.cardId, pattern)) {
3904
+ applyRequirement(card, `requireCard ${pattern}`);
3905
+ }
3485
3906
  }
3486
3907
  }
3487
3908
  }
3488
3909
  if (hints.requireTags?.length) {
3489
3910
  for (const pattern of hints.requireTags) {
3911
+ for (const cardId of cardIds) {
3912
+ const card = cardMap.get(cardId);
3913
+ if (cardMatchesTagPattern(card, pattern)) {
3914
+ applyRequirement(card, `requireTag ${pattern}`);
3915
+ }
3916
+ }
3490
3917
  for (const card of allCards) {
3491
- if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
3918
+ if (cardMatchesTagPattern(card, pattern)) {
3919
+ applyRequirement(card, `requireTag ${pattern}`);
3920
+ }
3492
3921
  }
3493
3922
  }
3494
3923
  }