@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.
@@ -527,6 +527,7 @@ var PipelineDebugger_exports = {};
527
527
  __export(PipelineDebugger_exports, {
528
528
  buildRunReport: () => buildRunReport,
529
529
  captureRun: () => captureRun,
530
+ clearRunHistory: () => clearRunHistory,
530
531
  mountPipelineDebugger: () => mountPipelineDebugger,
531
532
  pipelineDebugAPI: () => pipelineDebugAPI,
532
533
  registerPipelineForDebug: () => registerPipelineForDebug
@@ -534,6 +535,9 @@ __export(PipelineDebugger_exports, {
534
535
  function registerPipelineForDebug(pipeline) {
535
536
  _activePipeline = pipeline;
536
537
  }
538
+ function clearRunHistory() {
539
+ runHistory.length = 0;
540
+ }
537
541
  function getOrigin(card) {
538
542
  const firstEntry = card.provenance[0];
539
543
  if (!firstEntry) return "unknown";
@@ -562,7 +566,7 @@ function parseCardElo(provenance) {
562
566
  }
563
567
  function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
564
568
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
565
- const cards = allCards.map((card) => ({
569
+ const toReport = (card) => ({
566
570
  cardId: card.cardId,
567
571
  courseId: card.courseId,
568
572
  origin: getOrigin(card),
@@ -572,7 +576,47 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
572
576
  provenance: card.provenance,
573
577
  tags: card.tags,
574
578
  selected: selectedIds.has(card.cardId)
575
- }));
579
+ });
580
+ const selectedReported = [];
581
+ const nearMissReported = [];
582
+ const discardedTailCards = [];
583
+ let nonSelectedSeen = 0;
584
+ for (const card of allCards) {
585
+ if (selectedIds.has(card.cardId)) {
586
+ selectedReported.push(toReport(card));
587
+ } else if (nonSelectedSeen < DISCARDED_KEEP_TOP) {
588
+ nearMissReported.push(toReport(card));
589
+ nonSelectedSeen++;
590
+ } else {
591
+ discardedTailCards.push(card);
592
+ }
593
+ }
594
+ const cards = [...selectedReported, ...nearMissReported];
595
+ let discardedTail;
596
+ if (discardedTailCards.length > 0) {
597
+ let scoreMin = Infinity;
598
+ let scoreMax = -Infinity;
599
+ let eloMin = Infinity;
600
+ let eloMax = -Infinity;
601
+ let eloSeen = false;
602
+ for (const c of discardedTailCards) {
603
+ if (c.score < scoreMin) scoreMin = c.score;
604
+ if (c.score > scoreMax) scoreMax = c.score;
605
+ const elo = parseCardElo(c.provenance);
606
+ if (elo !== void 0) {
607
+ eloSeen = true;
608
+ if (elo < eloMin) eloMin = elo;
609
+ if (elo > eloMax) eloMax = elo;
610
+ }
611
+ }
612
+ const eloFragment = eloSeen ? `, ELO ${eloMin}\u2013${eloMax}` : "";
613
+ discardedTail = {
614
+ count: discardedTailCards.length,
615
+ scoreRange: [scoreMin, scoreMax],
616
+ eloRange: eloSeen ? [eloMin, eloMax] : void 0,
617
+ 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.`
618
+ };
619
+ }
576
620
  const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
577
621
  const newSelected = selectedCards.filter((c) => getOrigin(c) === "new").length;
578
622
  return {
@@ -587,7 +631,8 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
587
631
  finalCount: selectedCards.length,
588
632
  reviewsSelected,
589
633
  newSelected,
590
- cards
634
+ cards,
635
+ discardedTail
591
636
  };
592
637
  }
593
638
  function formatProvenance(provenance) {
@@ -623,6 +668,44 @@ function printRunSummary(run) {
623
668
  );
624
669
  console.groupEnd();
625
670
  }
671
+ function escapeHtml(s) {
672
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
673
+ }
674
+ function escapeAttr(s) {
675
+ return escapeHtml(s).replace(/"/g, "&quot;");
676
+ }
677
+ function copyTextToClipboard(text, btn) {
678
+ const done = () => {
679
+ if (!btn) return;
680
+ const orig = btn.textContent ?? "Copy";
681
+ btn.textContent = "Copied!";
682
+ btn.classList.add("copied");
683
+ setTimeout(() => {
684
+ btn.textContent = orig;
685
+ btn.classList.remove("copied");
686
+ }, 1200);
687
+ };
688
+ const fallback = () => {
689
+ const ta = document.createElement("textarea");
690
+ ta.value = text;
691
+ ta.style.position = "fixed";
692
+ ta.style.opacity = "0";
693
+ document.body.appendChild(ta);
694
+ ta.select();
695
+ try {
696
+ document.execCommand("copy");
697
+ } catch (e) {
698
+ logger.warn(`[Pipeline Debug] Copy failed: ${e}`);
699
+ }
700
+ document.body.removeChild(ta);
701
+ done();
702
+ };
703
+ if (navigator.clipboard?.writeText) {
704
+ navigator.clipboard.writeText(text).then(done).catch(fallback);
705
+ } else {
706
+ fallback();
707
+ }
708
+ }
626
709
  function renderUI() {
627
710
  if (!_uiContainer) return;
628
711
  const runs = runHistory;
@@ -681,6 +764,13 @@ function renderUI() {
681
764
  #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
682
765
  #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
683
766
  #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; }
767
+ #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; }
768
+ #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; }
769
+ #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; }
770
+ #sk-pipeline-debugger .copy-btn:hover { background: #0b5ed7; }
771
+ #sk-pipeline-debugger .copy-btn.copied { background: #198754; }
772
+ #sk-pipeline-debugger .section-head { display: flex; align-items: center; justify-content: space-between; margin-top: 1rem; }
773
+ #sk-pipeline-debugger .section-head h3 { margin: 0; }
684
774
  `;
685
775
  const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
686
776
  (r, i) => `
@@ -688,6 +778,7 @@ function renderUI() {
688
778
  <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
689
779
  <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
690
780
  <small>${r.finalCount} cards selected</small>
781
+ ${r.hints?._label ? `<br/><span class="run-label" title="${escapeAttr(r.hints._label)}">${escapeHtml(r.hints._label)}</span>` : ""}
691
782
  </div>
692
783
  `
693
784
  ).join("");
@@ -696,11 +787,13 @@ function renderUI() {
696
787
  const filteredCards = selectedRun.cards.filter(
697
788
  (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
698
789
  );
790
+ const labelText = selectedRun.hints?._label ?? "(no label)";
699
791
  detailsHtml = `
700
792
  <h2>Run: ${selectedRun.runId}</h2>
793
+ <div class="label-banner" title="${escapeAttr(labelText)}">${escapeHtml(labelText)}</div>
701
794
  <p>
702
- <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
703
- <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
795
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
796
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
704
797
  <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
705
798
  </p>
706
799
 
@@ -715,7 +808,10 @@ function renderUI() {
715
808
  </table>
716
809
 
717
810
  ${selectedRun.hints ? `
718
- <h3>Ephemeral Hints</h3>
811
+ <div class="section-head">
812
+ <h3>Ephemeral Hints</h3>
813
+ <button class="copy-btn" onclick="window.skuilder.pipeline._copyConfig('${selectedRun.runId}', this)">Copy config</button>
814
+ </div>
719
815
  <table>
720
816
  ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
721
817
  ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
@@ -739,7 +835,10 @@ function renderUI() {
739
835
  </tbody>
740
836
  </table>
741
837
 
742
- <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
838
+ <div class="section-head">
839
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
840
+ <button class="copy-btn" onclick="window.skuilder.pipeline._copyResults('${selectedRun.runId}', this)">Copy results</button>
841
+ </div>
743
842
  <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
744
843
 
745
844
  <table>
@@ -785,7 +884,7 @@ function mountPipelineDebugger() {
785
884
  win.skuilder = win.skuilder || {};
786
885
  win.skuilder.pipeline = pipelineDebugAPI;
787
886
  }
788
- var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
887
+ var _activePipeline, MAX_RUNS, runHistory, DISCARDED_KEEP_TOP, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
789
888
  var init_PipelineDebugger = __esm({
790
889
  "src/core/navigators/PipelineDebugger.ts"() {
791
890
  "use strict";
@@ -794,6 +893,7 @@ var init_PipelineDebugger = __esm({
794
893
  _activePipeline = null;
795
894
  MAX_RUNS = 10;
796
895
  runHistory = [];
896
+ DISCARDED_KEEP_TOP = 25;
797
897
  _uiContainer = null;
798
898
  _selectedRunIndex = null;
799
899
  _cardSearchQuery = "";
@@ -858,7 +958,14 @@ var init_PipelineDebugger = __esm({
858
958
  return;
859
959
  }
860
960
  }
861
- logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
961
+ const runsWithTails = runHistory.filter((r) => r.discardedTail && r.discardedTail.count > 0);
962
+ if (runsWithTails.length > 0) {
963
+ logger.info(
964
+ `[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.`
965
+ );
966
+ } else {
967
+ logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
968
+ }
862
969
  },
863
970
  /**
864
971
  * Explain why reviews may or may not have been selected.
@@ -1163,6 +1270,50 @@ var init_PipelineDebugger = __esm({
1163
1270
  _cardSearchQuery = query;
1164
1271
  renderUI();
1165
1272
  },
1273
+ /**
1274
+ * Internal UI helpers
1275
+ * @internal
1276
+ */
1277
+ _copyConfig(runId, btn) {
1278
+ const run = runHistory.find((r) => r.runId === runId);
1279
+ if (!run) return;
1280
+ const payload = {
1281
+ runId: run.runId,
1282
+ timestamp: run.timestamp.toISOString(),
1283
+ courseId: run.courseId,
1284
+ courseName: run.courseName,
1285
+ hints: run.hints ?? null
1286
+ };
1287
+ copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
1288
+ },
1289
+ /**
1290
+ * Internal UI helpers
1291
+ * @internal
1292
+ *
1293
+ * Copies an "abridged" view of results: just the selected cards with their
1294
+ * generator, origin, final score, and the top provenance reason. Designed
1295
+ * for pasting into bug reports without flooding with full provenance.
1296
+ */
1297
+ _copyResults(runId, btn) {
1298
+ const run = runHistory.find((r) => r.runId === runId);
1299
+ if (!run) return;
1300
+ const selected = run.cards.filter((c) => c.selected).sort((a, b) => b.finalScore - a.finalScore).map((c) => ({
1301
+ cardId: c.cardId,
1302
+ generator: c.generator,
1303
+ origin: c.origin,
1304
+ score: Number(c.finalScore.toFixed(3)),
1305
+ topReason: c.provenance[0]?.reason ?? ""
1306
+ }));
1307
+ const payload = {
1308
+ runId: run.runId,
1309
+ label: run.hints?._label ?? null,
1310
+ finalCount: run.finalCount,
1311
+ newSelected: run.newSelected,
1312
+ reviewsSelected: run.reviewsSelected,
1313
+ selected
1314
+ };
1315
+ copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
1316
+ },
1166
1317
  /**
1167
1318
  * Show help.
1168
1319
  */
@@ -7080,12 +7231,12 @@ var init_courseDB3 = __esm({
7080
7231
  }
7081
7232
  async getWeightedCards(limit) {
7082
7233
  try {
7083
- const navigator = await this.createNavigator(this.userDB);
7234
+ const navigator2 = await this.createNavigator(this.userDB);
7084
7235
  if (this._pendingHints) {
7085
- navigator.setEphemeralHints(this._pendingHints);
7236
+ navigator2.setEphemeralHints(this._pendingHints);
7086
7237
  this._pendingHints = null;
7087
7238
  }
7088
- return navigator.getWeightedCards(limit);
7239
+ return navigator2.getWeightedCards(limit);
7089
7240
  } catch (e) {
7090
7241
  logger.error(`[static/courseDB] Error getting weighted cards: ${e}`);
7091
7242
  throw e;