@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.
@@ -364,6 +364,19 @@ interface PipelineRunReport {
364
364
  finalCount: number;
365
365
  reviewsSelected: number;
366
366
  newSelected: number;
367
+ /**
368
+ * Card data for inspection.
369
+ *
370
+ * To keep the in-memory ring buffer small, this contains:
371
+ * - All selected cards (the actual session output).
372
+ * - The top-N highest-scoring non-selected cards (see DISCARDED_KEEP_TOP),
373
+ * useful for understanding "near misses" and filter behavior.
374
+ *
375
+ * The remaining low-score tail of the candidate pool (mostly ELO-window
376
+ * pull remnants that filters scored down) is summarized in `discardedTail`
377
+ * rather than retained verbatim — each retained card carries a multi-KB
378
+ * provenance trail, and the tail is typically hundreds of cards per run.
379
+ */
367
380
  cards: Array<{
368
381
  cardId: string;
369
382
  courseId: string;
@@ -376,6 +389,25 @@ interface PipelineRunReport {
376
389
  tags?: string[];
377
390
  selected: boolean;
378
391
  }>;
392
+ /**
393
+ * Summary of the discarded tail of the candidate pool — cards that were
394
+ * generated and scored but neither selected nor retained in `cards`.
395
+ *
396
+ * Provides breadcrumbs for "where did card X go?" investigations without
397
+ * the memory cost of keeping every dropped candidate's provenance.
398
+ *
399
+ * Future: may carry a bloom filter of discarded cardIds so callers can
400
+ * ask "was X in this run's discard tail?" with bounded memory cost.
401
+ */
402
+ discardedTail?: {
403
+ count: number;
404
+ /** [min, max] finalScore across the discarded tail. */
405
+ scoreRange?: [number, number];
406
+ /** [min, max] cardElo across the discarded tail (where parseable). */
407
+ eloRange?: [number, number];
408
+ /** Human-readable note for console display. */
409
+ note: string;
410
+ };
379
411
  }
380
412
  /**
381
413
  * Console API object exposed on window.skuilder.pipeline
@@ -468,6 +500,20 @@ declare const pipelineDebugAPI: {
468
500
  * @internal
469
501
  */
470
502
  _setSearch(query: string): void;
503
+ /**
504
+ * Internal UI helpers
505
+ * @internal
506
+ */
507
+ _copyConfig(runId: string, btn?: HTMLElement): void;
508
+ /**
509
+ * Internal UI helpers
510
+ * @internal
511
+ *
512
+ * Copies an "abridged" view of results: just the selected cards with their
513
+ * generator, origin, final score, and the top provenance reason. Designed
514
+ * for pasting into bug reports without flooding with full provenance.
515
+ */
516
+ _copyResults(runId: string, btn?: HTMLElement): void;
471
517
  /**
472
518
  * Show help.
473
519
  */
@@ -364,6 +364,19 @@ interface PipelineRunReport {
364
364
  finalCount: number;
365
365
  reviewsSelected: number;
366
366
  newSelected: number;
367
+ /**
368
+ * Card data for inspection.
369
+ *
370
+ * To keep the in-memory ring buffer small, this contains:
371
+ * - All selected cards (the actual session output).
372
+ * - The top-N highest-scoring non-selected cards (see DISCARDED_KEEP_TOP),
373
+ * useful for understanding "near misses" and filter behavior.
374
+ *
375
+ * The remaining low-score tail of the candidate pool (mostly ELO-window
376
+ * pull remnants that filters scored down) is summarized in `discardedTail`
377
+ * rather than retained verbatim — each retained card carries a multi-KB
378
+ * provenance trail, and the tail is typically hundreds of cards per run.
379
+ */
367
380
  cards: Array<{
368
381
  cardId: string;
369
382
  courseId: string;
@@ -376,6 +389,25 @@ interface PipelineRunReport {
376
389
  tags?: string[];
377
390
  selected: boolean;
378
391
  }>;
392
+ /**
393
+ * Summary of the discarded tail of the candidate pool — cards that were
394
+ * generated and scored but neither selected nor retained in `cards`.
395
+ *
396
+ * Provides breadcrumbs for "where did card X go?" investigations without
397
+ * the memory cost of keeping every dropped candidate's provenance.
398
+ *
399
+ * Future: may carry a bloom filter of discarded cardIds so callers can
400
+ * ask "was X in this run's discard tail?" with bounded memory cost.
401
+ */
402
+ discardedTail?: {
403
+ count: number;
404
+ /** [min, max] finalScore across the discarded tail. */
405
+ scoreRange?: [number, number];
406
+ /** [min, max] cardElo across the discarded tail (where parseable). */
407
+ eloRange?: [number, number];
408
+ /** Human-readable note for console display. */
409
+ note: string;
410
+ };
379
411
  }
380
412
  /**
381
413
  * Console API object exposed on window.skuilder.pipeline
@@ -468,6 +500,20 @@ declare const pipelineDebugAPI: {
468
500
  * @internal
469
501
  */
470
502
  _setSearch(query: string): void;
503
+ /**
504
+ * Internal UI helpers
505
+ * @internal
506
+ */
507
+ _copyConfig(runId: string, btn?: HTMLElement): void;
508
+ /**
509
+ * Internal UI helpers
510
+ * @internal
511
+ *
512
+ * Copies an "abridged" view of results: just the selected cards with their
513
+ * generator, origin, final score, and the top provenance reason. Designed
514
+ * for pasting into bug reports without flooding with full provenance.
515
+ */
516
+ _copyResults(runId: string, btn?: HTMLElement): void;
471
517
  /**
472
518
  * Show help.
473
519
  */
@@ -724,6 +724,7 @@ var PipelineDebugger_exports = {};
724
724
  __export(PipelineDebugger_exports, {
725
725
  buildRunReport: () => buildRunReport,
726
726
  captureRun: () => captureRun,
727
+ clearRunHistory: () => clearRunHistory,
727
728
  mountPipelineDebugger: () => mountPipelineDebugger,
728
729
  pipelineDebugAPI: () => pipelineDebugAPI,
729
730
  registerPipelineForDebug: () => registerPipelineForDebug
@@ -731,6 +732,9 @@ __export(PipelineDebugger_exports, {
731
732
  function registerPipelineForDebug(pipeline) {
732
733
  _activePipeline = pipeline;
733
734
  }
735
+ function clearRunHistory() {
736
+ runHistory.length = 0;
737
+ }
734
738
  function getOrigin(card) {
735
739
  const firstEntry = card.provenance[0];
736
740
  if (!firstEntry) return "unknown";
@@ -759,7 +763,7 @@ function parseCardElo(provenance) {
759
763
  }
760
764
  function buildRunReport(courseId, courseName, generatorName, generators, generatedCount, filters, allCards, selectedCards, userElo, hints) {
761
765
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
762
- const cards = allCards.map((card) => ({
766
+ const toReport = (card) => ({
763
767
  cardId: card.cardId,
764
768
  courseId: card.courseId,
765
769
  origin: getOrigin(card),
@@ -769,7 +773,47 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
769
773
  provenance: card.provenance,
770
774
  tags: card.tags,
771
775
  selected: selectedIds.has(card.cardId)
772
- }));
776
+ });
777
+ const selectedReported = [];
778
+ const nearMissReported = [];
779
+ const discardedTailCards = [];
780
+ let nonSelectedSeen = 0;
781
+ for (const card of allCards) {
782
+ if (selectedIds.has(card.cardId)) {
783
+ selectedReported.push(toReport(card));
784
+ } else if (nonSelectedSeen < DISCARDED_KEEP_TOP) {
785
+ nearMissReported.push(toReport(card));
786
+ nonSelectedSeen++;
787
+ } else {
788
+ discardedTailCards.push(card);
789
+ }
790
+ }
791
+ const cards = [...selectedReported, ...nearMissReported];
792
+ let discardedTail;
793
+ if (discardedTailCards.length > 0) {
794
+ let scoreMin = Infinity;
795
+ let scoreMax = -Infinity;
796
+ let eloMin = Infinity;
797
+ let eloMax = -Infinity;
798
+ let eloSeen = false;
799
+ for (const c of discardedTailCards) {
800
+ if (c.score < scoreMin) scoreMin = c.score;
801
+ if (c.score > scoreMax) scoreMax = c.score;
802
+ const elo = parseCardElo(c.provenance);
803
+ if (elo !== void 0) {
804
+ eloSeen = true;
805
+ if (elo < eloMin) eloMin = elo;
806
+ if (elo > eloMax) eloMax = elo;
807
+ }
808
+ }
809
+ const eloFragment = eloSeen ? `, ELO ${eloMin}\u2013${eloMax}` : "";
810
+ discardedTail = {
811
+ count: discardedTailCards.length,
812
+ scoreRange: [scoreMin, scoreMax],
813
+ eloRange: eloSeen ? [eloMin, eloMax] : void 0,
814
+ 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.`
815
+ };
816
+ }
773
817
  const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
774
818
  const newSelected = selectedCards.filter((c) => getOrigin(c) === "new").length;
775
819
  return {
@@ -784,7 +828,8 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
784
828
  finalCount: selectedCards.length,
785
829
  reviewsSelected,
786
830
  newSelected,
787
- cards
831
+ cards,
832
+ discardedTail
788
833
  };
789
834
  }
790
835
  function formatProvenance(provenance) {
@@ -820,6 +865,44 @@ function printRunSummary(run) {
820
865
  );
821
866
  console.groupEnd();
822
867
  }
868
+ function escapeHtml(s) {
869
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
870
+ }
871
+ function escapeAttr(s) {
872
+ return escapeHtml(s).replace(/"/g, "&quot;");
873
+ }
874
+ function copyTextToClipboard(text, btn) {
875
+ const done = () => {
876
+ if (!btn) return;
877
+ const orig = btn.textContent ?? "Copy";
878
+ btn.textContent = "Copied!";
879
+ btn.classList.add("copied");
880
+ setTimeout(() => {
881
+ btn.textContent = orig;
882
+ btn.classList.remove("copied");
883
+ }, 1200);
884
+ };
885
+ const fallback = () => {
886
+ const ta = document.createElement("textarea");
887
+ ta.value = text;
888
+ ta.style.position = "fixed";
889
+ ta.style.opacity = "0";
890
+ document.body.appendChild(ta);
891
+ ta.select();
892
+ try {
893
+ document.execCommand("copy");
894
+ } catch (e) {
895
+ logger.warn(`[Pipeline Debug] Copy failed: ${e}`);
896
+ }
897
+ document.body.removeChild(ta);
898
+ done();
899
+ };
900
+ if (navigator.clipboard?.writeText) {
901
+ navigator.clipboard.writeText(text).then(done).catch(fallback);
902
+ } else {
903
+ fallback();
904
+ }
905
+ }
823
906
  function renderUI() {
824
907
  if (!_uiContainer) return;
825
908
  const runs = runHistory;
@@ -878,6 +961,13 @@ function renderUI() {
878
961
  #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
879
962
  #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
880
963
  #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; }
964
+ #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; }
965
+ #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; }
966
+ #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; }
967
+ #sk-pipeline-debugger .copy-btn:hover { background: #0b5ed7; }
968
+ #sk-pipeline-debugger .copy-btn.copied { background: #198754; }
969
+ #sk-pipeline-debugger .section-head { display: flex; align-items: center; justify-content: space-between; margin-top: 1rem; }
970
+ #sk-pipeline-debugger .section-head h3 { margin: 0; }
881
971
  `;
882
972
  const runListHtml = runs.length === 0 ? '<div style="padding: 1rem;">No runs captured yet.</div>' : runs.map(
883
973
  (r, i) => `
@@ -885,6 +975,7 @@ function renderUI() {
885
975
  <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
886
976
  <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
887
977
  <small>${r.finalCount} cards selected</small>
978
+ ${r.hints?._label ? `<br/><span class="run-label" title="${escapeAttr(r.hints._label)}">${escapeHtml(r.hints._label)}</span>` : ""}
888
979
  </div>
889
980
  `
890
981
  ).join("");
@@ -893,11 +984,13 @@ function renderUI() {
893
984
  const filteredCards = selectedRun.cards.filter(
894
985
  (c) => !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
895
986
  );
987
+ const labelText = selectedRun.hints?._label ?? "(no label)";
896
988
  detailsHtml = `
897
989
  <h2>Run: ${selectedRun.runId}</h2>
990
+ <div class="label-banner" title="${escapeAttr(labelText)}">${escapeHtml(labelText)}</div>
898
991
  <p>
899
- <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
900
- <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
992
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
993
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
901
994
  <strong>User ELO:</strong> ${selectedRun.userElo ?? "unknown"}
902
995
  </p>
903
996
 
@@ -912,7 +1005,10 @@ function renderUI() {
912
1005
  </table>
913
1006
 
914
1007
  ${selectedRun.hints ? `
915
- <h3>Ephemeral Hints</h3>
1008
+ <div class="section-head">
1009
+ <h3>Ephemeral Hints</h3>
1010
+ <button class="copy-btn" onclick="window.skuilder.pipeline._copyConfig('${selectedRun.runId}', this)">Copy config</button>
1011
+ </div>
916
1012
  <table>
917
1013
  ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ""}
918
1014
  ${selectedRun.hints.boostTags ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>` : ""}
@@ -936,7 +1032,10 @@ function renderUI() {
936
1032
  </tbody>
937
1033
  </table>
938
1034
 
939
- <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
1035
+ <div class="section-head">
1036
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
1037
+ <button class="copy-btn" onclick="window.skuilder.pipeline._copyResults('${selectedRun.runId}', this)">Copy results</button>
1038
+ </div>
940
1039
  <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
941
1040
 
942
1041
  <table>
@@ -982,7 +1081,7 @@ function mountPipelineDebugger() {
982
1081
  win.skuilder = win.skuilder || {};
983
1082
  win.skuilder.pipeline = pipelineDebugAPI;
984
1083
  }
985
- var _activePipeline, MAX_RUNS, runHistory, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
1084
+ var _activePipeline, MAX_RUNS, runHistory, DISCARDED_KEEP_TOP, _uiContainer, _selectedRunIndex, _cardSearchQuery, pipelineDebugAPI;
986
1085
  var init_PipelineDebugger = __esm({
987
1086
  "src/core/navigators/PipelineDebugger.ts"() {
988
1087
  "use strict";
@@ -991,6 +1090,7 @@ var init_PipelineDebugger = __esm({
991
1090
  _activePipeline = null;
992
1091
  MAX_RUNS = 10;
993
1092
  runHistory = [];
1093
+ DISCARDED_KEEP_TOP = 25;
994
1094
  _uiContainer = null;
995
1095
  _selectedRunIndex = null;
996
1096
  _cardSearchQuery = "";
@@ -1055,7 +1155,14 @@ var init_PipelineDebugger = __esm({
1055
1155
  return;
1056
1156
  }
1057
1157
  }
1058
- logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
1158
+ const runsWithTails = runHistory.filter((r) => r.discardedTail && r.discardedTail.count > 0);
1159
+ if (runsWithTails.length > 0) {
1160
+ logger.info(
1161
+ `[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.`
1162
+ );
1163
+ } else {
1164
+ logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
1165
+ }
1059
1166
  },
1060
1167
  /**
1061
1168
  * Explain why reviews may or may not have been selected.
@@ -1360,6 +1467,50 @@ var init_PipelineDebugger = __esm({
1360
1467
  _cardSearchQuery = query;
1361
1468
  renderUI();
1362
1469
  },
1470
+ /**
1471
+ * Internal UI helpers
1472
+ * @internal
1473
+ */
1474
+ _copyConfig(runId, btn) {
1475
+ const run = runHistory.find((r) => r.runId === runId);
1476
+ if (!run) return;
1477
+ const payload = {
1478
+ runId: run.runId,
1479
+ timestamp: run.timestamp.toISOString(),
1480
+ courseId: run.courseId,
1481
+ courseName: run.courseName,
1482
+ hints: run.hints ?? null
1483
+ };
1484
+ copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
1485
+ },
1486
+ /**
1487
+ * Internal UI helpers
1488
+ * @internal
1489
+ *
1490
+ * Copies an "abridged" view of results: just the selected cards with their
1491
+ * generator, origin, final score, and the top provenance reason. Designed
1492
+ * for pasting into bug reports without flooding with full provenance.
1493
+ */
1494
+ _copyResults(runId, btn) {
1495
+ const run = runHistory.find((r) => r.runId === runId);
1496
+ if (!run) return;
1497
+ const selected = run.cards.filter((c) => c.selected).sort((a, b) => b.finalScore - a.finalScore).map((c) => ({
1498
+ cardId: c.cardId,
1499
+ generator: c.generator,
1500
+ origin: c.origin,
1501
+ score: Number(c.finalScore.toFixed(3)),
1502
+ topReason: c.provenance[0]?.reason ?? ""
1503
+ }));
1504
+ const payload = {
1505
+ runId: run.runId,
1506
+ label: run.hints?._label ?? null,
1507
+ finalCount: run.finalCount,
1508
+ newSelected: run.newSelected,
1509
+ reviewsSelected: run.reviewsSelected,
1510
+ selected
1511
+ };
1512
+ copyTextToClipboard(JSON.stringify(payload, null, 2), btn);
1513
+ },
1363
1514
  /**
1364
1515
  * Show help.
1365
1516
  */
@@ -5624,12 +5775,12 @@ ${e.stack}` : JSON.stringify(e);
5624
5775
  async getWeightedCards(limit) {
5625
5776
  const u = await this._getCurrentUser();
5626
5777
  try {
5627
- const navigator = await this.createNavigator(u);
5778
+ const navigator2 = await this.createNavigator(u);
5628
5779
  if (this._pendingHints) {
5629
- navigator.setEphemeralHints(this._pendingHints);
5780
+ navigator2.setEphemeralHints(this._pendingHints);
5630
5781
  this._pendingHints = null;
5631
5782
  }
5632
- return navigator.getWeightedCards(limit);
5783
+ return navigator2.getWeightedCards(limit);
5633
5784
  } catch (e) {
5634
5785
  logger.error(`[courseDB] Error getting weighted cards: ${e}`);
5635
5786
  throw e;