@vue-skuilder/db 0.1.35 → 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.
@@ -648,6 +648,7 @@ var PipelineDebugger_exports = {};
648
648
  __export(PipelineDebugger_exports, {
649
649
  buildRunReport: () => buildRunReport,
650
650
  captureRun: () => captureRun,
651
+ clearRunHistory: () => clearRunHistory,
651
652
  mountPipelineDebugger: () => mountPipelineDebugger,
652
653
  pipelineDebugAPI: () => pipelineDebugAPI,
653
654
  registerPipelineForDebug: () => registerPipelineForDebug
@@ -655,6 +656,9 @@ __export(PipelineDebugger_exports, {
655
656
  function registerPipelineForDebug(pipeline) {
656
657
  _activePipeline = pipeline;
657
658
  }
659
+ function clearRunHistory() {
660
+ runHistory.length = 0;
661
+ }
658
662
  function getOrigin(card) {
659
663
  const firstEntry = card.provenance[0];
660
664
  if (!firstEntry) return "unknown";
@@ -683,7 +687,7 @@ function parseCardElo(provenance) {
683
687
  }
684
688
  function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
685
689
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
686
- const cards = allCards.map((card) => ({
690
+ const toReport = (card) => ({
687
691
  cardId: card.cardId,
688
692
  courseId: card.courseId,
689
693
  origin: getOrigin(card),
@@ -693,7 +697,47 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
693
697
  provenance: card.provenance,
694
698
  tags: card.tags,
695
699
  selected: selectedIds.has(card.cardId)
696
- }));
700
+ });
701
+ const selectedReported = [];
702
+ const nearMissReported = [];
703
+ const discardedTailCards = [];
704
+ let nonSelectedSeen = 0;
705
+ for (const card of allCards) {
706
+ if (selectedIds.has(card.cardId)) {
707
+ selectedReported.push(toReport(card));
708
+ } else if (nonSelectedSeen < DISCARDED_KEEP_TOP) {
709
+ nearMissReported.push(toReport(card));
710
+ nonSelectedSeen++;
711
+ } else {
712
+ discardedTailCards.push(card);
713
+ }
714
+ }
715
+ const cards = [...selectedReported, ...nearMissReported];
716
+ let discardedTail;
717
+ if (discardedTailCards.length > 0) {
718
+ let scoreMin = Infinity;
719
+ let scoreMax = -Infinity;
720
+ let eloMin = Infinity;
721
+ let eloMax = -Infinity;
722
+ let eloSeen = false;
723
+ for (const c of discardedTailCards) {
724
+ if (c.score < scoreMin) scoreMin = c.score;
725
+ if (c.score > scoreMax) scoreMax = c.score;
726
+ const elo = parseCardElo(c.provenance);
727
+ if (elo !== void 0) {
728
+ eloSeen = true;
729
+ if (elo < eloMin) eloMin = elo;
730
+ if (elo > eloMax) eloMax = elo;
731
+ }
732
+ }
733
+ const eloFragment = eloSeen ? `, ELO ${eloMin}\u2013${eloMax}` : "";
734
+ discardedTail = {
735
+ count: discardedTailCards.length,
736
+ scoreRange: [scoreMin, scoreMax],
737
+ eloRange: eloSeen ? [eloMin, eloMax] : void 0,
738
+ 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.`
739
+ };
740
+ }
697
741
  const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
698
742
  const newSelected = selectedCards.filter((c) => getOrigin(c) === "new").length;
699
743
  return {
@@ -708,7 +752,8 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
708
752
  finalCount: selectedCards.length,
709
753
  reviewsSelected,
710
754
  newSelected,
711
- cards
755
+ cards,
756
+ discardedTail
712
757
  };
713
758
  }
714
759
  function formatProvenance(provenance) {
@@ -744,6 +789,44 @@ function printRunSummary(run) {
744
789
  );
745
790
  console.groupEnd();
746
791
  }
792
+ function escapeHtml(s) {
793
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
794
+ }
795
+ function escapeAttr(s) {
796
+ return escapeHtml(s).replace(/"/g, "&quot;");
797
+ }
798
+ function copyTextToClipboard(text, btn) {
799
+ const done = () => {
800
+ if (!btn) return;
801
+ const orig = btn.textContent ?? "Copy";
802
+ btn.textContent = "Copied!";
803
+ btn.classList.add("copied");
804
+ setTimeout(() => {
805
+ btn.textContent = orig;
806
+ btn.classList.remove("copied");
807
+ }, 1200);
808
+ };
809
+ const fallback = () => {
810
+ const ta = document.createElement("textarea");
811
+ ta.value = text;
812
+ ta.style.position = "fixed";
813
+ ta.style.opacity = "0";
814
+ document.body.appendChild(ta);
815
+ ta.select();
816
+ try {
817
+ document.execCommand("copy");
818
+ } catch (e) {
819
+ logger.warn(`[Pipeline Debug] Copy failed: ${e}`);
820
+ }
821
+ document.body.removeChild(ta);
822
+ done();
823
+ };
824
+ if (navigator.clipboard?.writeText) {
825
+ navigator.clipboard.writeText(text).then(done).catch(fallback);
826
+ } else {
827
+ fallback();
828
+ }
829
+ }
747
830
  function renderUI() {
748
831
  if (!_uiContainer) return;
749
832
  const runs = runHistory;
@@ -802,6 +885,13 @@ function renderUI() {
802
885
  #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
803
886
  #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
804
887
  #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; }
888
+ #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; }
889
+ #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; }
890
+ #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; }
891
+ #sk-pipeline-debugger .copy-btn:hover { background: #0b5ed7; }
892
+ #sk-pipeline-debugger .copy-btn.copied { background: #198754; }
893
+ #sk-pipeline-debugger .section-head { display: flex; align-items: center; justify-content: space-between; margin-top: 1rem; }
894
+ #sk-pipeline-debugger .section-head h3 { margin: 0; }
805
895
  `;
806
896
  const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
807
897
  (r, i) => `
@@ -809,6 +899,7 @@ function renderUI() {
809
899
  <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
810
900
  <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
811
901
  <small>${r.finalCount} cards selected</small>
902
+ ${r.hints?._label ? `<br/><span class="run-label" title="${escapeAttr(r.hints._label)}">${escapeHtml(r.hints._label)}</span>` : ""}
812
903
  </div>
813
904
  `
814
905
  ).join("");
@@ -817,11 +908,13 @@ function renderUI() {
817
908
  const filteredCards = selectedRun.cards.filter(
818
909
  (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
819
910
  );
911
+ const labelText = selectedRun.hints?._label ?? "(no label)";
820
912
  detailsHtml = `
821
913
  <h2>Run: ${selectedRun.runId}</h2>
914
+ <div class="label-banner" title="${escapeAttr(labelText)}">${escapeHtml(labelText)}</div>
822
915
  <p>
823
- <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
824
- <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
916
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
917
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
825
918
  <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
826
919
  </p>
827
920
 
@@ -836,7 +929,10 @@ function renderUI() {
836
929
  </table>
837
930
 
838
931
  ${selectedRun.hints ? `
839
- <h3>Ephemeral Hints</h3>
932
+ <div class="section-head">
933
+ <h3>Ephemeral Hints</h3>
934
+ <button class="copy-btn" onclick="window.skuilder.pipeline._copyConfig('${selectedRun.runId}', this)">Copy config</button>
935
+ </div>
840
936
  <table>
841
937
  ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
842
938
  ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
@@ -860,7 +956,10 @@ function renderUI() {
860
956
  </tbody>
861
957
  </table>
862
958
 
863
- <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
959
+ <div class="section-head">
960
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
961
+ <button class="copy-btn" onclick="window.skuilder.pipeline._copyResults('${selectedRun.runId}', this)">Copy results</button>
962
+ </div>
864
963
  <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
865
964
 
866
965
  <table>
@@ -906,7 +1005,7 @@ function mountPipelineDebugger() {
906
1005
  win.skuilder = win.skuilder || {};
907
1006
  win.skuilder.pipeline = pipelineDebugAPI;
908
1007
  }
909
- var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
1008
+ var _activePipeline, MAX_RUNS, runHistory, DISCARDED_KEEP_TOP, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
910
1009
  var init_PipelineDebugger = __esm({
911
1010
  "src/core/navigators/PipelineDebugger.ts"() {
912
1011
  "use strict";
@@ -915,6 +1014,7 @@ var init_PipelineDebugger = __esm({
915
1014
  _activePipeline = null;
916
1015
  MAX_RUNS = 10;
917
1016
  runHistory = [];
1017
+ DISCARDED_KEEP_TOP = 25;
918
1018
  _uiContainer = null;
919
1019
  _selectedRunIndex = null;
920
1020
  _cardSearchQuery = "";
@@ -979,7 +1079,14 @@ var init_PipelineDebugger = __esm({
979
1079
  return;
980
1080
  }
981
1081
  }
982
- logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
1082
+ const runsWithTails = runHistory.filter((r) => r.discardedTail && r.discardedTail.count > 0);
1083
+ if (runsWithTails.length > 0) {
1084
+ logger.info(
1085
+ `[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.`
1086
+ );
1087
+ } else {
1088
+ logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
1089
+ }
983
1090
  },
984
1091
  /**
985
1092
  * Explain why reviews may or may not have been selected.
@@ -1284,6 +1391,50 @@ var init_PipelineDebugger = __esm({
1284
1391
  _cardSearchQuery = query;
1285
1392
  renderUI();
1286
1393
  },
1394
+ /**
1395
+ * Internal UI helpers
1396
+ * @internal
1397
+ */
1398
+ _copyConfig(runId, btn) {
1399
+ const run = runHistory.find((r) => r.runId === runId);
1400
+ if (!run) return;
1401
+ const payload = {
1402
+ runId: run.runId,
1403
+ timestamp: run.timestamp.toISOString(),
1404
+ courseId: run.courseId,
1405
+ courseName: run.courseName,
1406
+ hints: run.hints ?? null
1407
+ };
1408
+ copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
1409
+ },
1410
+ /**
1411
+ * Internal UI helpers
1412
+ * @internal
1413
+ *
1414
+ * Copies an "abridged" view of results: just the selected cards with their
1415
+ * generator, origin, final score, and the top provenance reason. Designed
1416
+ * for pasting into bug reports without flooding with full provenance.
1417
+ */
1418
+ _copyResults(runId, btn) {
1419
+ const run = runHistory.find((r) => r.runId === runId);
1420
+ if (!run) return;
1421
+ const selected = run.cards.filter((c) => c.selected).sort((a, b) => b.finalScore - a.finalScore).map((c) => ({
1422
+ cardId: c.cardId,
1423
+ generator: c.generator,
1424
+ origin: c.origin,
1425
+ score: Number(c.finalScore.toFixed(3)),
1426
+ topReason: c.provenance[0]?.reason ?? ""
1427
+ }));
1428
+ const payload = {
1429
+ runId: run.runId,
1430
+ label: run.hints?._label ?? null,
1431
+ finalCount: run.finalCount,
1432
+ newSelected: run.newSelected,
1433
+ reviewsSelected: run.reviewsSelected,
1434
+ selected
1435
+ };
1436
+ copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
1437
+ },
1287
1438
  /**
1288
1439
  * Show help.
1289
1440
  */
@@ -5389,12 +5540,12 @@ ${e.stack}` : JSON.stringify(e);
5389
5540
  async getWeightedCards(limit) {
5390
5541
  const u = await this._getCurrentUser();
5391
5542
  try {
5392
- const navigator = await this.createNavigator(u);
5543
+ const navigator2 = await this.createNavigator(u);
5393
5544
  if (this._pendingHints) {
5394
- navigator.setEphemeralHints(this._pendingHints);
5545
+ navigator2.setEphemeralHints(this._pendingHints);
5395
5546
  this._pendingHints = null;
5396
5547
  }
5397
- return navigator.getWeightedCards(limit);
5548
+ return navigator2.getWeightedCards(limit);
5398
5549
  } catch (e) {
5399
5550
  logger.error(`[courseDB] Error getting weighted cards: ${e}`);
5400
5551
  throw e;