@vue-skuilder/db 0.1.34 → 0.1.36

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.
@@ -701,6 +701,7 @@ var PipelineDebugger_exports = {};
701
701
  __export(PipelineDebugger_exports, {
702
702
  buildRunReport: () => buildRunReport,
703
703
  captureRun: () => captureRun,
704
+ clearRunHistory: () => clearRunHistory,
704
705
  mountPipelineDebugger: () => mountPipelineDebugger,
705
706
  pipelineDebugAPI: () => pipelineDebugAPI,
706
707
  registerPipelineForDebug: () => registerPipelineForDebug
@@ -708,6 +709,9 @@ __export(PipelineDebugger_exports, {
708
709
  function registerPipelineForDebug(pipeline) {
709
710
  _activePipeline = pipeline;
710
711
  }
712
+ function clearRunHistory() {
713
+ runHistory.length = 0;
714
+ }
711
715
  function getOrigin(card) {
712
716
  const firstEntry = card.provenance[0];
713
717
  if (!firstEntry) return "unknown";
@@ -736,7 +740,7 @@ function parseCardElo(provenance) {
736
740
  }
737
741
  function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
738
742
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
739
- const cards = allCards.map((card) => ({
743
+ const toReport = (card) => ({
740
744
  cardId: card.cardId,
741
745
  courseId: card.courseId,
742
746
  origin: getOrigin(card),
@@ -746,7 +750,47 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
746
750
  provenance: card.provenance,
747
751
  tags: card.tags,
748
752
  selected: selectedIds.has(card.cardId)
749
- }));
753
+ });
754
+ const selectedReported = [];
755
+ const nearMissReported = [];
756
+ const discardedTailCards = [];
757
+ let nonSelectedSeen = 0;
758
+ for (const card of allCards) {
759
+ if (selectedIds.has(card.cardId)) {
760
+ selectedReported.push(toReport(card));
761
+ } else if (nonSelectedSeen < DISCARDED_KEEP_TOP) {
762
+ nearMissReported.push(toReport(card));
763
+ nonSelectedSeen++;
764
+ } else {
765
+ discardedTailCards.push(card);
766
+ }
767
+ }
768
+ const cards = [...selectedReported, ...nearMissReported];
769
+ let discardedTail;
770
+ if (discardedTailCards.length > 0) {
771
+ let scoreMin = Infinity;
772
+ let scoreMax = -Infinity;
773
+ let eloMin = Infinity;
774
+ let eloMax = -Infinity;
775
+ let eloSeen = false;
776
+ for (const c of discardedTailCards) {
777
+ if (c.score < scoreMin) scoreMin = c.score;
778
+ if (c.score > scoreMax) scoreMax = c.score;
779
+ const elo = parseCardElo(c.provenance);
780
+ if (elo !== void 0) {
781
+ eloSeen = true;
782
+ if (elo < eloMin) eloMin = elo;
783
+ if (elo > eloMax) eloMax = elo;
784
+ }
785
+ }
786
+ const eloFragment = eloSeen ? `, ELO ${eloMin}\u2013${eloMax}` : "";
787
+ discardedTail = {
788
+ count: discardedTailCards.length,
789
+ scoreRange: [scoreMin, scoreMax],
790
+ eloRange: eloSeen ? [eloMin, eloMax] : void 0,
791
+ note: `${discardedTailCards.length} additional candidate(s) scored below the top ${DISCARDED_KEEP_TOP} near-misses and were not retained (score ${scoreMin.toExponential(2)}\u2013${scoreMax.toExponential(2)}${eloFragment}). Likely ELO-window pull remnants filtered out by hierarchy/lesson/priority gates.`
792
+ };
793
+ }
750
794
  const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
751
795
  const newSelected = selectedCards.filter((c) => getOrigin(c) === "new").length;
752
796
  return {
@@ -761,7 +805,8 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
761
805
  finalCount: selectedCards.length,
762
806
  reviewsSelected,
763
807
  newSelected,
764
- cards
808
+ cards,
809
+ discardedTail
765
810
  };
766
811
  }
767
812
  function formatProvenance(provenance) {
@@ -797,6 +842,44 @@ function printRunSummary(run) {
797
842
  );
798
843
  console.groupEnd();
799
844
  }
845
+ function escapeHtml(s) {
846
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
847
+ }
848
+ function escapeAttr(s) {
849
+ return escapeHtml(s).replace(/"/g, "&quot;");
850
+ }
851
+ function copyTextToClipboard(text, btn) {
852
+ const done = () => {
853
+ if (!btn) return;
854
+ const orig = btn.textContent ?? "Copy";
855
+ btn.textContent = "Copied!";
856
+ btn.classList.add("copied");
857
+ setTimeout(() => {
858
+ btn.textContent = orig;
859
+ btn.classList.remove("copied");
860
+ }, 1200);
861
+ };
862
+ const fallback = () => {
863
+ const ta = document.createElement("textarea");
864
+ ta.value = text;
865
+ ta.style.position = "fixed";
866
+ ta.style.opacity = "0";
867
+ document.body.appendChild(ta);
868
+ ta.select();
869
+ try {
870
+ document.execCommand("copy");
871
+ } catch (e) {
872
+ logger.warn(`[Pipeline Debug] Copy failed: ${e}`);
873
+ }
874
+ document.body.removeChild(ta);
875
+ done();
876
+ };
877
+ if (navigator.clipboard?.writeText) {
878
+ navigator.clipboard.writeText(text).then(done).catch(fallback);
879
+ } else {
880
+ fallback();
881
+ }
882
+ }
800
883
  function renderUI() {
801
884
  if (!_uiContainer) return;
802
885
  const runs = runHistory;
@@ -855,6 +938,13 @@ function renderUI() {
855
938
  #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
856
939
  #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
857
940
  #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; }
941
+ #sk-pipeline-debugger .run-label { display: inline-block; margin-top: 0.25rem; padding: 0.1rem 0.4rem; background: #fff3cd; color: #664d03; border-radius: 3px; font-family: monospace; font-size: 11px; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; vertical-align: bottom; }
942
+ #sk-pipeline-debugger .label-banner { display: inline-block; padding: 0.25rem 0.6rem; background: #fff3cd; color: #664d03; border-radius: 4px; font-family: monospace; font-size: 13px; margin: 0 0 0.75rem 0; }
943
+ #sk-pipeline-debugger .copy-btn { background: #0d6efd; color: white; border: none; padding: 0.25rem 0.6rem; border-radius: 3px; cursor: pointer; font-size: 12px; margin-left: 0.5rem; }
944
+ #sk-pipeline-debugger .copy-btn:hover { background: #0b5ed7; }
945
+ #sk-pipeline-debugger .copy-btn.copied { background: #198754; }
946
+ #sk-pipeline-debugger .section-head { display: flex; align-items: center; justify-content: space-between; margin-top: 1rem; }
947
+ #sk-pipeline-debugger .section-head h3 { margin: 0; }
858
948
  `;
859
949
  const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
860
950
  (r, i) => `
@@ -862,6 +952,7 @@ function renderUI() {
862
952
  <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
863
953
  <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
864
954
  <small>${r.finalCount} cards selected</small>
955
+ ${r.hints?._label ? `<br/><span class="run-label" title="${escapeAttr(r.hints._label)}">${escapeHtml(r.hints._label)}</span>` : ""}
865
956
  </div>
866
957
  `
867
958
  ).join("");
@@ -870,11 +961,13 @@ function renderUI() {
870
961
  const filteredCards = selectedRun.cards.filter(
871
962
  (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
872
963
  );
964
+ const labelText = selectedRun.hints?._label ?? "(no label)";
873
965
  detailsHtml = `
874
966
  <h2>Run: ${selectedRun.runId}</h2>
967
+ <div class="label-banner" title="${escapeAttr(labelText)}">${escapeHtml(labelText)}</div>
875
968
  <p>
876
- <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
877
- <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
969
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
970
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
878
971
  <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
879
972
  </p>
880
973
 
@@ -889,7 +982,10 @@ function renderUI() {
889
982
  </table>
890
983
 
891
984
  ${selectedRun.hints ? `
892
- <h3>Ephemeral Hints</h3>
985
+ <div class="section-head">
986
+ <h3>Ephemeral Hints</h3>
987
+ <button class="copy-btn" onclick="window.skuilder.pipeline._copyConfig('${selectedRun.runId}', this)">Copy config</button>
988
+ </div>
893
989
  <table>
894
990
  ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
895
991
  ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
@@ -913,7 +1009,10 @@ function renderUI() {
913
1009
  </tbody>
914
1010
  </table>
915
1011
 
916
- <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
1012
+ <div class="section-head">
1013
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
1014
+ <button class="copy-btn" onclick="window.skuilder.pipeline._copyResults('${selectedRun.runId}', this)">Copy results</button>
1015
+ </div>
917
1016
  <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
918
1017
 
919
1018
  <table>
@@ -959,7 +1058,7 @@ function mountPipelineDebugger() {
959
1058
  win.skuilder = win.skuilder || {};
960
1059
  win.skuilder.pipeline = pipelineDebugAPI;
961
1060
  }
962
- var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
1061
+ var _activePipeline, MAX_RUNS, runHistory, DISCARDED_KEEP_TOP, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
963
1062
  var init_PipelineDebugger = __esm({
964
1063
  "src/core/navigators/PipelineDebugger.ts"() {
965
1064
  "use strict";
@@ -968,6 +1067,7 @@ var init_PipelineDebugger = __esm({
968
1067
  _activePipeline = null;
969
1068
  MAX_RUNS = 10;
970
1069
  runHistory = [];
1070
+ DISCARDED_KEEP_TOP = 25;
971
1071
  _uiContainer = null;
972
1072
  _selectedRunIndex = null;
973
1073
  _cardSearchQuery = "";
@@ -1032,7 +1132,14 @@ var init_PipelineDebugger = __esm({
1032
1132
  return;
1033
1133
  }
1034
1134
  }
1035
- logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
1135
+ const runsWithTails = runHistory.filter((r) => r.discardedTail && r.discardedTail.count > 0);
1136
+ if (runsWithTails.length > 0) {
1137
+ logger.info(
1138
+ `[Pipeline Debug] Card '${cardId}' not found in retained cards. ${runsWithTails.length} run(s) have discarded tails that were not retained \u2014 the card may have been a low-score candidate. See run.discardedTail for ranges.`
1139
+ );
1140
+ } else {
1141
+ logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
1142
+ }
1036
1143
  },
1037
1144
  /**
1038
1145
  * Explain why reviews may or may not have been selected.
@@ -1337,6 +1444,50 @@ var init_PipelineDebugger = __esm({
1337
1444
  _cardSearchQuery = query;
1338
1445
  renderUI();
1339
1446
  },
1447
+ /**
1448
+ * Internal UI helpers
1449
+ * @internal
1450
+ */
1451
+ _copyConfig(runId, btn) {
1452
+ const run = runHistory.find((r) => r.runId === runId);
1453
+ if (!run) return;
1454
+ const payload = {
1455
+ runId: run.runId,
1456
+ timestamp: run.timestamp.toISOString(),
1457
+ courseId: run.courseId,
1458
+ courseName: run.courseName,
1459
+ hints: run.hints ?? null
1460
+ };
1461
+ copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
1462
+ },
1463
+ /**
1464
+ * Internal UI helpers
1465
+ * @internal
1466
+ *
1467
+ * Copies an "abridged" view of results: just the selected cards with their
1468
+ * generator, origin, final score, and the top provenance reason. Designed
1469
+ * for pasting into bug reports without flooding with full provenance.
1470
+ */
1471
+ _copyResults(runId, btn) {
1472
+ const run = runHistory.find((r) => r.runId === runId);
1473
+ if (!run) return;
1474
+ const selected = run.cards.filter((c) => c.selected).sort((a, b) => b.finalScore - a.finalScore).map((c) => ({
1475
+ cardId: c.cardId,
1476
+ generator: c.generator,
1477
+ origin: c.origin,
1478
+ score: Number(c.finalScore.toFixed(3)),
1479
+ topReason: c.provenance[0]?.reason ?? ""
1480
+ }));
1481
+ const payload = {
1482
+ runId: run.runId,
1483
+ label: run.hints?._label ?? null,
1484
+ finalCount: run.finalCount,
1485
+ newSelected: run.newSelected,
1486
+ reviewsSelected: run.reviewsSelected,
1487
+ selected
1488
+ };
1489
+ copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
1490
+ },
1340
1491
  /**
1341
1492
  * Show help.
1342
1493
  */
@@ -5606,12 +5757,12 @@ ${e.stack}` : JSON.stringify(e);
5606
5757
  async getWeightedCards(limit) {
5607
5758
  const u = await this._getCurrentUser();
5608
5759
  try {
5609
- const navigator = await this.createNavigator(u);
5760
+ const navigator2 = await this.createNavigator(u);
5610
5761
  if (this._pendingHints) {
5611
- navigator.setEphemeralHints(this._pendingHints);
5762
+ navigator2.setEphemeralHints(this._pendingHints);
5612
5763
  this._pendingHints = null;
5613
5764
  }
5614
- return navigator.getWeightedCards(limit);
5765
+ return navigator2.getWeightedCards(limit);
5615
5766
  } catch (e) {
5616
5767
  logger.error(`[courseDB] Error getting weighted cards: ${e}`);
5617
5768
  throw e;