@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.
@@ -503,6 +503,7 @@ var PipelineDebugger_exports = {};
503
503
  __export(PipelineDebugger_exports, {
504
504
  buildRunReport: () => buildRunReport,
505
505
  captureRun: () => captureRun,
506
+ clearRunHistory: () => clearRunHistory,
506
507
  mountPipelineDebugger: () => mountPipelineDebugger,
507
508
  pipelineDebugAPI: () => pipelineDebugAPI,
508
509
  registerPipelineForDebug: () => registerPipelineForDebug
@@ -510,6 +511,9 @@ __export(PipelineDebugger_exports, {
510
511
  function registerPipelineForDebug(pipeline) {
511
512
  _activePipeline = pipeline;
512
513
  }
514
+ function clearRunHistory() {
515
+ runHistory.length = 0;
516
+ }
513
517
  function getOrigin(card) {
514
518
  const firstEntry = card.provenance[0];
515
519
  if (!firstEntry) return "unknown";
@@ -538,7 +542,7 @@ function parseCardElo(provenance) {
538
542
  }
539
543
  function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
540
544
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
541
- const cards = allCards.map((card) => ({
545
+ const toReport = (card) => ({
542
546
  cardId: card.cardId,
543
547
  courseId: card.courseId,
544
548
  origin: getOrigin(card),
@@ -548,7 +552,47 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
548
552
  provenance: card.provenance,
549
553
  tags: card.tags,
550
554
  selected: selectedIds.has(card.cardId)
551
- }));
555
+ });
556
+ const selectedReported = [];
557
+ const nearMissReported = [];
558
+ const discardedTailCards = [];
559
+ let nonSelectedSeen = 0;
560
+ for (const card of allCards) {
561
+ if (selectedIds.has(card.cardId)) {
562
+ selectedReported.push(toReport(card));
563
+ } else if (nonSelectedSeen < DISCARDED_KEEP_TOP) {
564
+ nearMissReported.push(toReport(card));
565
+ nonSelectedSeen++;
566
+ } else {
567
+ discardedTailCards.push(card);
568
+ }
569
+ }
570
+ const cards = [...selectedReported, ...nearMissReported];
571
+ let discardedTail;
572
+ if (discardedTailCards.length > 0) {
573
+ let scoreMin = Infinity;
574
+ let scoreMax = -Infinity;
575
+ let eloMin = Infinity;
576
+ let eloMax = -Infinity;
577
+ let eloSeen = false;
578
+ for (const c of discardedTailCards) {
579
+ if (c.score < scoreMin) scoreMin = c.score;
580
+ if (c.score > scoreMax) scoreMax = c.score;
581
+ const elo = parseCardElo(c.provenance);
582
+ if (elo !== void 0) {
583
+ eloSeen = true;
584
+ if (elo < eloMin) eloMin = elo;
585
+ if (elo > eloMax) eloMax = elo;
586
+ }
587
+ }
588
+ const eloFragment = eloSeen ? `, ELO ${eloMin}\u2013${eloMax}` : "";
589
+ discardedTail = {
590
+ count: discardedTailCards.length,
591
+ scoreRange: [scoreMin, scoreMax],
592
+ eloRange: eloSeen ? [eloMin, eloMax] : void 0,
593
+ 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.`
594
+ };
595
+ }
552
596
  const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
553
597
  const newSelected = selectedCards.filter((c) => getOrigin(c) === "new").length;
554
598
  return {
@@ -563,7 +607,8 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
563
607
  finalCount: selectedCards.length,
564
608
  reviewsSelected,
565
609
  newSelected,
566
- cards
610
+ cards,
611
+ discardedTail
567
612
  };
568
613
  }
569
614
  function formatProvenance(provenance) {
@@ -599,6 +644,44 @@ function printRunSummary(run) {
599
644
  );
600
645
  console.groupEnd();
601
646
  }
647
+ function escapeHtml(s) {
648
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
649
+ }
650
+ function escapeAttr(s) {
651
+ return escapeHtml(s).replace(/"/g, "&quot;");
652
+ }
653
+ function copyTextToClipboard(text, btn) {
654
+ const done = () => {
655
+ if (!btn) return;
656
+ const orig = btn.textContent ?? "Copy";
657
+ btn.textContent = "Copied!";
658
+ btn.classList.add("copied");
659
+ setTimeout(() => {
660
+ btn.textContent = orig;
661
+ btn.classList.remove("copied");
662
+ }, 1200);
663
+ };
664
+ const fallback = () => {
665
+ const ta = document.createElement("textarea");
666
+ ta.value = text;
667
+ ta.style.position = "fixed";
668
+ ta.style.opacity = "0";
669
+ document.body.appendChild(ta);
670
+ ta.select();
671
+ try {
672
+ document.execCommand("copy");
673
+ } catch (e) {
674
+ logger.warn(`[Pipeline Debug] Copy failed: ${e}`);
675
+ }
676
+ document.body.removeChild(ta);
677
+ done();
678
+ };
679
+ if (navigator.clipboard?.writeText) {
680
+ navigator.clipboard.writeText(text).then(done).catch(fallback);
681
+ } else {
682
+ fallback();
683
+ }
684
+ }
602
685
  function renderUI() {
603
686
  if (!_uiContainer) return;
604
687
  const runs = runHistory;
@@ -657,6 +740,13 @@ function renderUI() {
657
740
  #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
658
741
  #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
659
742
  #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; }
743
+ #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; }
744
+ #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; }
745
+ #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; }
746
+ #sk-pipeline-debugger .copy-btn:hover { background: #0b5ed7; }
747
+ #sk-pipeline-debugger .copy-btn.copied { background: #198754; }
748
+ #sk-pipeline-debugger .section-head { display: flex; align-items: center; justify-content: space-between; margin-top: 1rem; }
749
+ #sk-pipeline-debugger .section-head h3 { margin: 0; }
660
750
  `;
661
751
  const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
662
752
  (r, i) => `
@@ -664,6 +754,7 @@ function renderUI() {
664
754
  <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
665
755
  <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
666
756
  <small>${r.finalCount} cards selected</small>
757
+ ${r.hints?._label ? `<br/><span class="run-label" title="${escapeAttr(r.hints._label)}">${escapeHtml(r.hints._label)}</span>` : ""}
667
758
  </div>
668
759
  `
669
760
  ).join("");
@@ -672,11 +763,13 @@ function renderUI() {
672
763
  const filteredCards = selectedRun.cards.filter(
673
764
  (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
674
765
  );
766
+ const labelText = selectedRun.hints?._label ?? "(no label)";
675
767
  detailsHtml = `
676
768
  <h2>Run: ${selectedRun.runId}</h2>
769
+ <div class="label-banner" title="${escapeAttr(labelText)}">${escapeHtml(labelText)}</div>
677
770
  <p>
678
- <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
679
- <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
771
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
772
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
680
773
  <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
681
774
  </p>
682
775
 
@@ -691,7 +784,10 @@ function renderUI() {
691
784
  </table>
692
785
 
693
786
  ${selectedRun.hints ? `
694
- <h3>Ephemeral Hints</h3>
787
+ <div class="section-head">
788
+ <h3>Ephemeral Hints</h3>
789
+ <button class="copy-btn" onclick="window.skuilder.pipeline._copyConfig('${selectedRun.runId}', this)">Copy config</button>
790
+ </div>
695
791
  <table>
696
792
  ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
697
793
  ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
@@ -715,7 +811,10 @@ function renderUI() {
715
811
  </tbody>
716
812
  </table>
717
813
 
718
- <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
814
+ <div class="section-head">
815
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
816
+ <button class="copy-btn" onclick="window.skuilder.pipeline._copyResults('${selectedRun.runId}', this)">Copy results</button>
817
+ </div>
719
818
  <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
720
819
 
721
820
  <table>
@@ -761,7 +860,7 @@ function mountPipelineDebugger() {
761
860
  win.skuilder = win.skuilder || {};
762
861
  win.skuilder.pipeline = pipelineDebugAPI;
763
862
  }
764
- var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
863
+ var _activePipeline, MAX_RUNS, runHistory, DISCARDED_KEEP_TOP, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
765
864
  var init_PipelineDebugger = __esm({
766
865
  "src/core/navigators/PipelineDebugger.ts"() {
767
866
  "use strict";
@@ -770,6 +869,7 @@ var init_PipelineDebugger = __esm({
770
869
  _activePipeline = null;
771
870
  MAX_RUNS = 10;
772
871
  runHistory = [];
872
+ DISCARDED_KEEP_TOP = 25;
773
873
  _uiContainer = null;
774
874
  _selectedRunIndex = null;
775
875
  _cardSearchQuery = "";
@@ -834,7 +934,14 @@ var init_PipelineDebugger = __esm({
834
934
  return;
835
935
  }
836
936
  }
837
- logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
937
+ const runsWithTails = runHistory.filter((r) => r.discardedTail && r.discardedTail.count > 0);
938
+ if (runsWithTails.length > 0) {
939
+ logger.info(
940
+ `[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.`
941
+ );
942
+ } else {
943
+ logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
944
+ }
838
945
  },
839
946
  /**
840
947
  * Explain why reviews may or may not have been selected.
@@ -1139,6 +1246,50 @@ var init_PipelineDebugger = __esm({
1139
1246
  _cardSearchQuery = query;
1140
1247
  renderUI();
1141
1248
  },
1249
+ /**
1250
+ * Internal UI helpers
1251
+ * @internal
1252
+ */
1253
+ _copyConfig(runId, btn) {
1254
+ const run = runHistory.find((r) => r.runId === runId);
1255
+ if (!run) return;
1256
+ const payload = {
1257
+ runId: run.runId,
1258
+ timestamp: run.timestamp.toISOString(),
1259
+ courseId: run.courseId,
1260
+ courseName: run.courseName,
1261
+ hints: run.hints ?? null
1262
+ };
1263
+ copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
1264
+ },
1265
+ /**
1266
+ * Internal UI helpers
1267
+ * @internal
1268
+ *
1269
+ * Copies an "abridged" view of results: just the selected cards with their
1270
+ * generator, origin, final score, and the top provenance reason. Designed
1271
+ * for pasting into bug reports without flooding with full provenance.
1272
+ */
1273
+ _copyResults(runId, btn) {
1274
+ const run = runHistory.find((r) => r.runId === runId);
1275
+ if (!run) return;
1276
+ const selected = run.cards.filter((c) => c.selected).sort((a, b) => b.finalScore - a.finalScore).map((c) => ({
1277
+ cardId: c.cardId,
1278
+ generator: c.generator,
1279
+ origin: c.origin,
1280
+ score: Number(c.finalScore.toFixed(3)),
1281
+ topReason: c.provenance[0]?.reason ?? ""
1282
+ }));
1283
+ const payload = {
1284
+ runId: run.runId,
1285
+ label: run.hints?._label ?? null,
1286
+ finalCount: run.finalCount,
1287
+ newSelected: run.newSelected,
1288
+ reviewsSelected: run.reviewsSelected,
1289
+ selected
1290
+ };
1291
+ copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
1292
+ },
1142
1293
  /**
1143
1294
  * Show help.
1144
1295
  */
@@ -7054,12 +7205,12 @@ var init_courseDB3 = __esm({
7054
7205
  }
7055
7206
  async getWeightedCards(limit) {
7056
7207
  try {
7057
- const navigator = await this.createNavigator(this.userDB);
7208
+ const navigator2 = await this.createNavigator(this.userDB);
7058
7209
  if (this._pendingHints) {
7059
- navigator.setEphemeralHints(this._pendingHints);
7210
+ navigator2.setEphemeralHints(this._pendingHints);
7060
7211
  this._pendingHints = null;
7061
7212
  }
7062
- return navigator.getWeightedCards(limit);
7213
+ return navigator2.getWeightedCards(limit);
7063
7214
  } catch (e) {
7064
7215
  logger.error(`[static/courseDB] Error getting weighted cards: ${e}`);
7065
7216
  throw e;