@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.
@@ -626,6 +626,7 @@ var PipelineDebugger_exports = {};
626
626
  __export(PipelineDebugger_exports, {
627
627
  buildRunReport: () => buildRunReport,
628
628
  captureRun: () => captureRun,
629
+ clearRunHistory: () => clearRunHistory,
629
630
  mountPipelineDebugger: () => mountPipelineDebugger,
630
631
  pipelineDebugAPI: () => pipelineDebugAPI,
631
632
  registerPipelineForDebug: () => registerPipelineForDebug
@@ -633,6 +634,9 @@ __export(PipelineDebugger_exports, {
633
634
  function registerPipelineForDebug(pipeline) {
634
635
  _activePipeline = pipeline;
635
636
  }
637
+ function clearRunHistory() {
638
+ runHistory.length = 0;
639
+ }
636
640
  function getOrigin(card) {
637
641
  const firstEntry = card.provenance[0];
638
642
  if (!firstEntry) return "unknown";
@@ -661,7 +665,7 @@ function parseCardElo(provenance) {
661
665
  }
662
666
  function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
663
667
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
664
- const cards = allCards.map((card) => ({
668
+ const toReport = (card) => ({
665
669
  cardId: card.cardId,
666
670
  courseId: card.courseId,
667
671
  origin: getOrigin(card),
@@ -671,7 +675,47 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
671
675
  provenance: card.provenance,
672
676
  tags: card.tags,
673
677
  selected: selectedIds.has(card.cardId)
674
- }));
678
+ });
679
+ const selectedReported = [];
680
+ const nearMissReported = [];
681
+ const discardedTailCards = [];
682
+ let nonSelectedSeen = 0;
683
+ for (const card of allCards) {
684
+ if (selectedIds.has(card.cardId)) {
685
+ selectedReported.push(toReport(card));
686
+ } else if (nonSelectedSeen < DISCARDED_KEEP_TOP) {
687
+ nearMissReported.push(toReport(card));
688
+ nonSelectedSeen++;
689
+ } else {
690
+ discardedTailCards.push(card);
691
+ }
692
+ }
693
+ const cards = [...selectedReported, ...nearMissReported];
694
+ let discardedTail;
695
+ if (discardedTailCards.length > 0) {
696
+ let scoreMin = Infinity;
697
+ let scoreMax = -Infinity;
698
+ let eloMin = Infinity;
699
+ let eloMax = -Infinity;
700
+ let eloSeen = false;
701
+ for (const c of discardedTailCards) {
702
+ if (c.score < scoreMin) scoreMin = c.score;
703
+ if (c.score > scoreMax) scoreMax = c.score;
704
+ const elo = parseCardElo(c.provenance);
705
+ if (elo !== void 0) {
706
+ eloSeen = true;
707
+ if (elo < eloMin) eloMin = elo;
708
+ if (elo > eloMax) eloMax = elo;
709
+ }
710
+ }
711
+ const eloFragment = eloSeen ? `, ELO ${eloMin}\u2013${eloMax}` : "";
712
+ discardedTail = {
713
+ count: discardedTailCards.length,
714
+ scoreRange: [scoreMin, scoreMax],
715
+ eloRange: eloSeen ? [eloMin, eloMax] : void 0,
716
+ 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.`
717
+ };
718
+ }
675
719
  const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
676
720
  const newSelected = selectedCards.filter((c) => getOrigin(c) === "new").length;
677
721
  return {
@@ -686,7 +730,8 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
686
730
  finalCount: selectedCards.length,
687
731
  reviewsSelected,
688
732
  newSelected,
689
- cards
733
+ cards,
734
+ discardedTail
690
735
  };
691
736
  }
692
737
  function formatProvenance(provenance) {
@@ -722,6 +767,44 @@ function printRunSummary(run) {
722
767
  );
723
768
  console.groupEnd();
724
769
  }
770
+ function escapeHtml(s) {
771
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
772
+ }
773
+ function escapeAttr(s) {
774
+ return escapeHtml(s).replace(/"/g, "&quot;");
775
+ }
776
+ function copyTextToClipboard(text, btn) {
777
+ const done = () => {
778
+ if (!btn) return;
779
+ const orig = btn.textContent ?? "Copy";
780
+ btn.textContent = "Copied!";
781
+ btn.classList.add("copied");
782
+ setTimeout(() => {
783
+ btn.textContent = orig;
784
+ btn.classList.remove("copied");
785
+ }, 1200);
786
+ };
787
+ const fallback = () => {
788
+ const ta = document.createElement("textarea");
789
+ ta.value = text;
790
+ ta.style.position = "fixed";
791
+ ta.style.opacity = "0";
792
+ document.body.appendChild(ta);
793
+ ta.select();
794
+ try {
795
+ document.execCommand("copy");
796
+ } catch (e) {
797
+ logger.warn(`[Pipeline Debug] Copy failed: ${e}`);
798
+ }
799
+ document.body.removeChild(ta);
800
+ done();
801
+ };
802
+ if (navigator.clipboard?.writeText) {
803
+ navigator.clipboard.writeText(text).then(done).catch(fallback);
804
+ } else {
805
+ fallback();
806
+ }
807
+ }
725
808
  function renderUI() {
726
809
  if (!_uiContainer) return;
727
810
  const runs = runHistory;
@@ -780,6 +863,13 @@ function renderUI() {
780
863
  #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
781
864
  #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
782
865
  #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; }
866
+ #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; }
867
+ #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; }
868
+ #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; }
869
+ #sk-pipeline-debugger .copy-btn:hover { background: #0b5ed7; }
870
+ #sk-pipeline-debugger .copy-btn.copied { background: #198754; }
871
+ #sk-pipeline-debugger .section-head { display: flex; align-items: center; justify-content: space-between; margin-top: 1rem; }
872
+ #sk-pipeline-debugger .section-head h3 { margin: 0; }
783
873
  `;
784
874
  const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
785
875
  (r, i) => `
@@ -787,6 +877,7 @@ function renderUI() {
787
877
  <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
788
878
  <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
789
879
  <small>${r.finalCount} cards selected</small>
880
+ ${r.hints?._label ? `<br/><span class="run-label" title="${escapeAttr(r.hints._label)}">${escapeHtml(r.hints._label)}</span>` : ""}
790
881
  </div>
791
882
  `
792
883
  ).join("");
@@ -795,11 +886,13 @@ function renderUI() {
795
886
  const filteredCards = selectedRun.cards.filter(
796
887
  (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
797
888
  );
889
+ const labelText = selectedRun.hints?._label ?? "(no label)";
798
890
  detailsHtml = `
799
891
  <h2>Run: ${selectedRun.runId}</h2>
892
+ <div class="label-banner" title="${escapeAttr(labelText)}">${escapeHtml(labelText)}</div>
800
893
  <p>
801
- <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
802
- <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
894
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
895
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
803
896
  <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
804
897
  </p>
805
898
 
@@ -814,7 +907,10 @@ function renderUI() {
814
907
  </table>
815
908
 
816
909
  ${selectedRun.hints ? `
817
- <h3>Ephemeral Hints</h3>
910
+ <div class="section-head">
911
+ <h3>Ephemeral Hints</h3>
912
+ <button class="copy-btn" onclick="window.skuilder.pipeline._copyConfig('${selectedRun.runId}', this)">Copy config</button>
913
+ </div>
818
914
  <table>
819
915
  ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
820
916
  ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
@@ -838,7 +934,10 @@ function renderUI() {
838
934
  </tbody>
839
935
  </table>
840
936
 
841
- <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
937
+ <div class="section-head">
938
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
939
+ <button class="copy-btn" onclick="window.skuilder.pipeline._copyResults('${selectedRun.runId}', this)">Copy results</button>
940
+ </div>
842
941
  <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
843
942
 
844
943
  <table>
@@ -884,7 +983,7 @@ function mountPipelineDebugger() {
884
983
  win.skuilder = win.skuilder || {};
885
984
  win.skuilder.pipeline = pipelineDebugAPI;
886
985
  }
887
- var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
986
+ var _activePipeline, MAX_RUNS, runHistory, DISCARDED_KEEP_TOP, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
888
987
  var init_PipelineDebugger = __esm({
889
988
  "src/core/navigators/PipelineDebugger.ts"() {
890
989
  "use strict";
@@ -893,6 +992,7 @@ var init_PipelineDebugger = __esm({
893
992
  _activePipeline = null;
894
993
  MAX_RUNS = 10;
895
994
  runHistory = [];
995
+ DISCARDED_KEEP_TOP = 25;
896
996
  _uiContainer = null;
897
997
  _selectedRunIndex = null;
898
998
  _cardSearchQuery = "";
@@ -957,7 +1057,14 @@ var init_PipelineDebugger = __esm({
957
1057
  return;
958
1058
  }
959
1059
  }
960
- logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
1060
+ const runsWithTails = runHistory.filter((r) => r.discardedTail && r.discardedTail.count > 0);
1061
+ if (runsWithTails.length > 0) {
1062
+ logger.info(
1063
+ `[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.`
1064
+ );
1065
+ } else {
1066
+ logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
1067
+ }
961
1068
  },
962
1069
  /**
963
1070
  * Explain why reviews may or may not have been selected.
@@ -1262,6 +1369,50 @@ var init_PipelineDebugger = __esm({
1262
1369
  _cardSearchQuery = query;
1263
1370
  renderUI();
1264
1371
  },
1372
+ /**
1373
+ * Internal UI helpers
1374
+ * @internal
1375
+ */
1376
+ _copyConfig(runId, btn) {
1377
+ const run = runHistory.find((r) => r.runId === runId);
1378
+ if (!run) return;
1379
+ const payload = {
1380
+ runId: run.runId,
1381
+ timestamp: run.timestamp.toISOString(),
1382
+ courseId: run.courseId,
1383
+ courseName: run.courseName,
1384
+ hints: run.hints ?? null
1385
+ };
1386
+ copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
1387
+ },
1388
+ /**
1389
+ * Internal UI helpers
1390
+ * @internal
1391
+ *
1392
+ * Copies an "abridged" view of results: just the selected cards with their
1393
+ * generator, origin, final score, and the top provenance reason. Designed
1394
+ * for pasting into bug reports without flooding with full provenance.
1395
+ */
1396
+ _copyResults(runId, btn) {
1397
+ const run = runHistory.find((r) => r.runId === runId);
1398
+ if (!run) return;
1399
+ const selected = run.cards.filter((c) => c.selected).sort((a, b) => b.finalScore - a.finalScore).map((c) => ({
1400
+ cardId: c.cardId,
1401
+ generator: c.generator,
1402
+ origin: c.origin,
1403
+ score: Number(c.finalScore.toFixed(3)),
1404
+ topReason: c.provenance[0]?.reason ?? ""
1405
+ }));
1406
+ const payload = {
1407
+ runId: run.runId,
1408
+ label: run.hints?._label ?? null,
1409
+ finalCount: run.finalCount,
1410
+ newSelected: run.newSelected,
1411
+ reviewsSelected: run.reviewsSelected,
1412
+ selected
1413
+ };
1414
+ copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
1415
+ },
1265
1416
  /**
1266
1417
  * Show help.
1267
1418
  */
@@ -5372,12 +5523,12 @@ ${e.stack}` : JSON.stringify(e);
5372
5523
  async getWeightedCards(limit) {
5373
5524
  const u = await this._getCurrentUser();
5374
5525
  try {
5375
- const navigator = await this.createNavigator(u);
5526
+ const navigator2 = await this.createNavigator(u);
5376
5527
  if (this._pendingHints) {
5377
- navigator.setEphemeralHints(this._pendingHints);
5528
+ navigator2.setEphemeralHints(this._pendingHints);
5378
5529
  this._pendingHints = null;
5379
5530
  }
5380
- return navigator.getWeightedCards(limit);
5531
+ return navigator2.getWeightedCards(limit);
5381
5532
  } catch (e) {
5382
5533
  logger.error(`[courseDB] Error getting weighted cards: ${e}`);
5383
5534
  throw e;