@vue-skuilder/db 0.1.32-e → 0.1.32-f

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.
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
7
- "version": "0.1.32-e",
7
+ "version": "0.1.32-f",
8
8
  "description": "Database layer for vue-skuilder",
9
9
  "main": "dist/index.js",
10
10
  "module": "dist/index.mjs",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@nilock2/pouchdb-authentication": "^1.0.2",
51
- "@vue-skuilder/common": "0.1.32-e",
51
+ "@vue-skuilder/common": "0.1.32-f",
52
52
  "cross-fetch": "^4.1.0",
53
53
  "moment": "^2.29.4",
54
54
  "pouchdb": "^9.0.0",
@@ -59,8 +59,8 @@
59
59
  "@types/uuid": "^10.0.0",
60
60
  "tsup": "^8.0.2",
61
61
  "typescript": "~5.9.3",
62
- "vite": "^7.0.0",
63
- "vitest": "^4.0.15"
62
+ "vite": "^8.0.0",
63
+ "vitest": "^4.1.0"
64
64
  },
65
65
  "stableVersion": "0.1.31"
66
66
  }
@@ -406,17 +406,52 @@ export class Pipeline extends ContentNavigator {
406
406
  // Keep a copy of all cards for debug capture (before filtering removes any)
407
407
  const allCardsBeforeFiltering = [...cards];
408
408
 
409
+ // Pre-fetch any literal requireCards IDs that the generator didn't produce.
410
+ // requireCards is a hard guarantee — required cards bypass the filter chain
411
+ // and are injected directly into the final result by applyHints. We need
412
+ // them present in allCardsBeforeFiltering for that injection to find them.
413
+ // (Glob patterns are left to the existing pool-search behaviour.)
414
+ const pendingHints = this._ephemeralHints;
415
+ if (pendingHints?.requireCards?.length) {
416
+ const poolIds = new Set(allCardsBeforeFiltering.map((c) => c.cardId));
417
+ const missingIds = pendingHints.requireCards.filter(
418
+ (p) => !p.includes('*') && !poolIds.has(p)
419
+ );
420
+ if (missingIds.length > 0) {
421
+ const fetchedTags = await this.course!.getAppliedTagsBatch(missingIds);
422
+ const courseId = this.course!.getCourseID();
423
+ for (const cardId of missingIds) {
424
+ allCardsBeforeFiltering.push({
425
+ cardId,
426
+ courseId,
427
+ score: 1.0,
428
+ tags: fetchedTags.get(cardId) ?? [],
429
+ provenance: [],
430
+ });
431
+ }
432
+ logger.info(
433
+ `[Pipeline] Pre-fetched ${missingIds.length} required card(s) into pool: ${missingIds.join(', ')}`
434
+ );
435
+ }
436
+ }
437
+
409
438
  // Apply filters sequentially, tracking impact
439
+ // Track prescribed-origin cards through the filter chain for diagnostics
440
+ const prescribedIds = new Set(
441
+ cards
442
+ .filter((c) => c.provenance.some((p) => p.strategy === 'prescribed'))
443
+ .map((c) => c.cardId)
444
+ );
410
445
  const filterImpacts: FilterImpact[] = [];
411
446
  for (const filter of this.filters) {
412
447
  const beforeCount = cards.length;
413
448
  const beforeScores = new Map(cards.map((c) => [c.cardId, c.score]));
414
449
  cards = await filter.transform(cards, context);
415
-
450
+
416
451
  // Count boost/penalize/pass/removed for this filter
417
452
  let boosted = 0, penalized = 0, passed = 0;
418
453
  const removed = beforeCount - cards.length;
419
-
454
+
420
455
  for (const card of cards) {
421
456
  const before = beforeScores.get(card.cardId) ?? 0;
422
457
  if (card.score > before) boosted++;
@@ -424,7 +459,25 @@ export class Pipeline extends ContentNavigator {
424
459
  else passed++;
425
460
  }
426
461
  filterImpacts.push({ name: filter.name, boosted, penalized, passed, removed });
427
-
462
+
463
+ // Report prescribed card fate through each filter
464
+ if (prescribedIds.size > 0) {
465
+ const survivingIds = new Set(cards.map((c) => c.cardId));
466
+ const killedPrescribed = [...prescribedIds].filter((id) => !survivingIds.has(id));
467
+ const zeroedPrescribed = cards
468
+ .filter((c) => prescribedIds.has(c.cardId) && c.score === 0)
469
+ .map((c) => c.cardId);
470
+ if (killedPrescribed.length > 0 || zeroedPrescribed.length > 0) {
471
+ logger.info(
472
+ `[Pipeline] Filter '${filter.name}' impact on prescribed cards: ` +
473
+ (killedPrescribed.length > 0 ? `removed=[${killedPrescribed.join(', ')}] ` : '') +
474
+ (zeroedPrescribed.length > 0 ? `zeroed=[${zeroedPrescribed.join(', ')}]` : '')
475
+ );
476
+ // Remove killed ones from tracking set
477
+ killedPrescribed.forEach((id) => prescribedIds.delete(id));
478
+ }
479
+ }
480
+
428
481
  logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} → ${cards.length} cards (↑${boosted} ↓${penalized} =${passed})`);
429
482
  }
430
483
 
@@ -483,7 +536,8 @@ export class Pipeline extends ContentNavigator {
483
536
  filterImpacts,
484
537
  cards,
485
538
  result,
486
- context.userElo
539
+ context.userElo,
540
+ hints?? undefined
487
541
  );
488
542
  captureRun(report);
489
543
  } catch (e) {
@@ -614,16 +668,33 @@ export class Pipeline extends ContentNavigator {
614
668
  }
615
669
  }
616
670
 
617
- // 3. Require — inject from the full pool if not already present
671
+ // 3. Require — ensure mandatory cards have floor score and are in the pool
618
672
  const cardIds = new Set(cards.map((c) => c.cardId));
673
+ const cardMap = new Map(cards.map((c) => [c.cardId, c]));
619
674
  const hintLabel = hints._label ? `Replan Hint (${hints._label})` : 'Replan Hint';
620
- const inject = (card: WeightedCard, reason: string) => {
621
- if (!cardIds.has(card.cardId)) {
622
- // Give required cards a floor score so they sort above zero-score filler
623
- const floorScore = Math.max(card.score, 1.0);
675
+
676
+ const applyRequirement = (card: WeightedCard, reason: string) => {
677
+ const mandatoryScore = Number.POSITIVE_INFINITY;
678
+ const existing = cardMap.get(card.cardId);
679
+
680
+ if (existing) {
681
+ // If already in the pool, upgrade to mandatory score if not already infinite
682
+ if (existing.score < mandatoryScore) {
683
+ existing.score = mandatoryScore;
684
+ existing.provenance.push({
685
+ strategy: 'ephemeralHint',
686
+ strategyId: 'ephemeral-hint',
687
+ strategyName: hintLabel,
688
+ action: 'boosted',
689
+ score: mandatoryScore,
690
+ reason: `${reason} (upgrade to mandatory score)`,
691
+ });
692
+ }
693
+ } else {
694
+ // If missing, inject from the full pool
624
695
  cards.push({
625
696
  ...card,
626
- score: floorScore,
697
+ score: mandatoryScore,
627
698
  provenance: [
628
699
  ...card.provenance,
629
700
  {
@@ -631,26 +702,46 @@ export class Pipeline extends ContentNavigator {
631
702
  strategyId: 'ephemeral-hint',
632
703
  strategyName: hintLabel,
633
704
  action: 'boosted',
634
- score: floorScore,
705
+ score: mandatoryScore,
635
706
  reason,
636
707
  },
637
708
  ],
638
709
  });
639
710
  cardIds.add(card.cardId);
711
+ cardMap.set(card.cardId, cards[cards.length - 1]);
640
712
  }
641
713
  };
642
714
 
643
715
  if (hints.requireCards?.length) {
644
716
  for (const pattern of hints.requireCards) {
717
+ // First check candidates already in the pool
718
+ for (const cardId of cardIds) {
719
+ if (globMatch(cardId, pattern)) {
720
+ applyRequirement(cardMap.get(cardId)!, `requireCard ${pattern}`);
721
+ }
722
+ }
723
+ // Then check full pool for injection
645
724
  for (const card of allCards) {
646
- if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
725
+ if (globMatch(card.cardId, pattern)) {
726
+ applyRequirement(card, `requireCard ${pattern}`);
727
+ }
647
728
  }
648
729
  }
649
730
  }
650
731
  if (hints.requireTags?.length) {
651
732
  for (const pattern of hints.requireTags) {
733
+ // First check candidates already in the pool
734
+ for (const cardId of cardIds) {
735
+ const card = cardMap.get(cardId)!;
736
+ if (cardMatchesTagPattern(card, pattern)) {
737
+ applyRequirement(card, `requireTag ${pattern}`);
738
+ }
739
+ }
740
+ // Then check full pool for injection
652
741
  for (const card of allCards) {
653
- if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
742
+ if (cardMatchesTagPattern(card, pattern)) {
743
+ applyRequirement(card, `requireTag ${pattern}`);
744
+ }
654
745
  }
655
746
  }
656
747
  }
@@ -9,6 +9,7 @@ import {
9
9
  } from './index';
10
10
  import { logger } from '../../util/logger';
11
11
  import type { Pipeline, CardSpaceDiagnosis } from './Pipeline';
12
+ import type { ReplanHints } from './generators/types';
12
13
 
13
14
  /**
14
15
  * Captured reference to the most recently created Pipeline instance.
@@ -83,6 +84,9 @@ export interface PipelineRunReport {
83
84
  // Filter phase
84
85
  filters: FilterImpact[];
85
86
 
87
+ /** Ephemeral hints applied during this run */
88
+ hints?: ReplanHints;
89
+
86
90
  // Results
87
91
  finalCount: number;
88
92
  reviewsSelected: number;
@@ -93,6 +97,7 @@ export interface PipelineRunReport {
93
97
  cardId: string;
94
98
  courseId: string;
95
99
  origin: 'new' | 'review' | 'unknown';
100
+ generator?: string;
96
101
  finalScore: number;
97
102
  /** Card's ELO (parsed from ELO generator provenance, if available) */
98
103
  cardElo?: number;
@@ -115,8 +120,10 @@ function getOrigin(card: WeightedCard): 'new' | 'review' | 'unknown' {
115
120
  const firstEntry = card.provenance[0];
116
121
  if (!firstEntry) return 'unknown';
117
122
  const reason = firstEntry.reason?.toLowerCase() || '';
118
- if (reason.includes('new card')) return 'new';
119
- if (reason.includes('review')) return 'review';
123
+ const strategy = firstEntry.strategy?.toLowerCase() || '';
124
+
125
+ if (reason.includes('new card') || strategy.includes('elo')) return 'new';
126
+ if (reason.includes('review') || strategy.includes('srs')) return 'review';
120
127
  return 'unknown';
121
128
  }
122
129
 
@@ -159,7 +166,8 @@ export function buildRunReport(
159
166
  filters: FilterImpact[],
160
167
  allCards: WeightedCard[],
161
168
  selectedCards: WeightedCard[],
162
- userElo?: number
169
+ userElo?: number,
170
+ hints?: ReplanHints
163
171
  ): Omit<PipelineRunReport, 'runId' | 'timestamp'> {
164
172
  const selectedIds = new Set(selectedCards.map((c) => c.cardId));
165
173
 
@@ -167,6 +175,7 @@ export function buildRunReport(
167
175
  cardId: card.cardId,
168
176
  courseId: card.courseId,
169
177
  origin: getOrigin(card),
178
+ generator: card.provenance[0]?.strategyName || card.provenance[0]?.strategy,
170
179
  finalScore: card.score,
171
180
  cardElo: parseCardElo(card.provenance),
172
181
  provenance: card.provenance,
@@ -185,6 +194,7 @@ export function buildRunReport(
185
194
  generators,
186
195
  generatedCount,
187
196
  filters,
197
+ hints,
188
198
  finalCount: selectedCards.length,
189
199
  reviewsSelected,
190
200
  newSelected,
@@ -255,6 +265,223 @@ function printRunSummary(run: PipelineRunReport): void {
255
265
  console.groupEnd();
256
266
  }
257
267
 
268
+ // ============================================================================
269
+ // UI DEBUGGER (VANILLA HTML)
270
+ // ============================================================================
271
+
272
+ let _uiContainer: HTMLElement | null = null;
273
+ let _selectedRunIndex: number | null = null;
274
+ let _cardSearchQuery = '';
275
+
276
+ function renderUI(): void {
277
+ if (!_uiContainer) return;
278
+
279
+ const runs = runHistory;
280
+ const selectedRun = _selectedRunIndex !== null ? runs[_selectedRunIndex] : null;
281
+
282
+ const styles = `
283
+ #sk-pipeline-debugger {
284
+ position: fixed;
285
+ top: 0;
286
+ left: 0;
287
+ width: 100vw;
288
+ height: 100vh;
289
+ background: #f8f9fa;
290
+ color: #212529;
291
+ z-index: 999999;
292
+ display: flex;
293
+ flex-direction: column;
294
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
295
+ font-size: 14px;
296
+ }
297
+ #sk-pipeline-debugger header {
298
+ padding: 1rem;
299
+ background: #343a40;
300
+ color: white;
301
+ display: flex;
302
+ justify-content: space-between;
303
+ align-items: center;
304
+ }
305
+ #sk-pipeline-debugger .container {
306
+ display: flex;
307
+ flex: 1;
308
+ overflow: hidden;
309
+ }
310
+ #sk-pipeline-debugger .sidebar {
311
+ width: 300px;
312
+ border-right: 1px solid #dee2e6;
313
+ overflow-y: auto;
314
+ background: white;
315
+ }
316
+ #sk-pipeline-debugger .main-content {
317
+ flex: 1;
318
+ overflow-y: auto;
319
+ padding: 1.5rem;
320
+ }
321
+ #sk-pipeline-debugger .run-item {
322
+ padding: 0.75rem 1rem;
323
+ border-bottom: 1px solid #eee;
324
+ cursor: pointer;
325
+ }
326
+ #sk-pipeline-debugger .run-item:hover { background: #f1f3f5; }
327
+ #sk-pipeline-debugger .run-item.active { background: #e9ecef; border-left: 4px solid #007bff; }
328
+ #sk-pipeline-debugger h2, #sk-pipeline-debugger h3 { margin-top: 0; }
329
+ #sk-pipeline-debugger table { width: 100%; border-collapse: collapse; margin-bottom: 1rem; background: white; }
330
+ #sk-pipeline-debugger th, #sk-pipeline-debugger td { border: 1px solid #dee2e6; padding: 0.5rem; text-align: left; }
331
+ #sk-pipeline-debugger th { background: #f1f3f5; }
332
+ #sk-pipeline-debugger code { background: #f1f3f5; padding: 0.1rem 0.3rem; border-radius: 3px; font-family: monospace; }
333
+ #sk-pipeline-debugger .close-btn { background: #dc3545; color: white; border: none; padding: 0.5rem 1rem; border-radius: 4px; cursor: pointer; }
334
+ #sk-pipeline-debugger .search-box { margin-bottom: 1rem; width: 100%; padding: 0.5rem; border: 1px solid #ced4da; border-radius: 4px; }
335
+ #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; }
336
+ `;
337
+
338
+ const runListHtml =
339
+ runs.length === 0
340
+ ? '<div style="padding: 1rem;">No runs captured yet.</div>'
341
+ : runs
342
+ .map(
343
+ (r, i) => `
344
+ <div class="run-item ${i === _selectedRunIndex ? 'active' : ''}" onclick="window.skuilder.pipeline._selectRun(${i})">
345
+ <strong>${r.timestamp.toLocaleTimeString()}</strong><br/>
346
+ <small>${r.courseName || r.courseId.slice(0, 8)}</small><br/>
347
+ <small>${r.finalCount} cards selected</small>
348
+ </div>
349
+ `
350
+ )
351
+ .join('');
352
+
353
+ let detailsHtml =
354
+ '<div style="color: #6c757d; text-align: center; margin-top: 5rem;">Select a run to see details</div>';
355
+
356
+ if (selectedRun) {
357
+ const filteredCards = selectedRun.cards.filter(
358
+ (c) =>
359
+ !_cardSearchQuery || c.cardId.toLowerCase().includes(_cardSearchQuery.toLowerCase())
360
+ );
361
+
362
+ detailsHtml = `
363
+ <h2>Run: ${selectedRun.runId}</h2>
364
+ <p>
365
+ <strong>Time:</strong> ${selectedRun.timestamp.toLocaleString()} |
366
+ <strong>Course:</strong> ${selectedRun.courseName || selectedRun.courseId} |
367
+ <strong>User ELO:</strong> ${selectedRun.userElo ?? 'unknown'}
368
+ </p>
369
+
370
+ <h3>Pipeline Config</h3>
371
+ <table>
372
+ <tr><th>Generator</th><td>${selectedRun.generatorName} (${selectedRun.generatedCount} candidates)</td></tr>
373
+ ${(selectedRun.generators || [])
374
+ .map(
375
+ (g) => `
376
+ <tr><td style="padding-left: 2rem;">↳ ${g.name}</td><td>${g.cardCount} cards (${g.newCount} new, ${g.reviewCount} review, top: ${g.topScore.toFixed(2)})</td></tr>
377
+ `
378
+ )
379
+ .join('')}
380
+ </table>
381
+
382
+ ${
383
+ selectedRun.hints
384
+ ? `
385
+ <h3>Ephemeral Hints</h3>
386
+ <table>
387
+ ${selectedRun.hints._label ? `<tr><th>Label</th><td>${selectedRun.hints._label}</td></tr>` : ''}
388
+ ${
389
+ selectedRun.hints.boostTags
390
+ ? `<tr><th>Boost Tags</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostTags, null, 2)}</pre></td></tr>`
391
+ : ''
392
+ }
393
+ ${
394
+ selectedRun.hints.boostCards
395
+ ? `<tr><th>Boost Cards</th><td><pre style="margin:0">${JSON.stringify(selectedRun.hints.boostCards, null, 2)}</pre></td></tr>`
396
+ : ''
397
+ }
398
+ ${
399
+ selectedRun.hints.requireTags
400
+ ? `<tr><th>Require Tags</th><td>${selectedRun.hints.requireTags.join(', ')}</td></tr>`
401
+ : ''
402
+ }
403
+ ${
404
+ selectedRun.hints.requireCards
405
+ ? `<tr><th>Require Cards</th><td>${selectedRun.hints.requireCards.join(', ')}</td></tr>`
406
+ : ''
407
+ }
408
+ ${
409
+ selectedRun.hints.excludeTags
410
+ ? `<tr><th>Exclude Tags</th><td>${selectedRun.hints.excludeTags.join(', ')}</td></tr>`
411
+ : ''
412
+ }
413
+ ${
414
+ selectedRun.hints.excludeCards
415
+ ? `<tr><th>Exclude Cards</th><td>${selectedRun.hints.excludeCards.join(', ')}</td></tr>`
416
+ : ''
417
+ }
418
+ </table>
419
+ `
420
+ : ''
421
+ }
422
+
423
+ <h3>Filter Impact</h3>
424
+ <table>
425
+ <thead><tr><th>Filter</th><th>Boosted</th><th>Penalized</th><th>Passed</th><th>Removed</th></tr></thead>
426
+ <tbody>
427
+ ${selectedRun.filters
428
+ .map(
429
+ (f) => `
430
+ <tr><td>${f.name}</td><td>↑${f.boosted}</td><td>↓${f.penalized}</td><td>=${f.passed}</td><td>✕${f.removed}</td></tr>
431
+ `
432
+ )
433
+ .join('')}
434
+ </tbody>
435
+ </table>
436
+
437
+ <h3>Cards (${selectedRun.finalCount} selected / ${selectedRun.cards.length} total)</h3>
438
+ <input type="text" class="search-box" placeholder="Search Card ID..." value="${_cardSearchQuery}" oninput="window.skuilder.pipeline._setSearch(this.value)">
439
+
440
+ <table>
441
+ <thead><tr><th>ID</th><th>Generator</th><th>Origin</th><th>Score</th><th>Selected</th></tr></thead>
442
+ <tbody>
443
+ ${filteredCards
444
+ .map(
445
+ (c) => `
446
+ <tr>
447
+ <td><code>${c.cardId}</code></td>
448
+ <td>${c.generator || 'unknown'}</td>
449
+ <td>${c.origin}</td>
450
+ <td>${c.finalScore.toFixed(3)}</td>
451
+ <td>${c.selected ? '✅' : '❌'}</td>
452
+ </tr>
453
+ ${
454
+ c.selected || _cardSearchQuery
455
+ ? `
456
+ <tr>
457
+ <td colspan="5">
458
+ <div class="provenance">${formatProvenance(c.provenance)}</div>
459
+ </td>
460
+ </tr>
461
+ `
462
+ : ''
463
+ }
464
+ `
465
+ )
466
+ .join('')}
467
+ </tbody>
468
+ </table>
469
+ `;
470
+ }
471
+
472
+ _uiContainer.innerHTML = `
473
+ <style>${styles}</style>
474
+ <header>
475
+ <strong>Pipeline Debugger</strong>
476
+ <button class="close-btn" onclick="window.skuilder.pipeline.ui()">Close</button>
477
+ </header>
478
+ <div class="container">
479
+ <div class="sidebar">${runListHtml}</div>
480
+ <div class="main-content">${detailsHtml}</div>
481
+ </div>
482
+ `;
483
+ }
484
+
258
485
  /**
259
486
  * Console API object exposed on window.skuilder.pipeline
260
487
  */
@@ -429,17 +656,26 @@ export const pipelineDebugAPI = {
429
656
  const mode = reason.match(/mode=([^;]+)/)?.[1] ?? 'unknown';
430
657
  const blocked = reason.match(/blocked=([^;]+)/)?.[1] ?? 'unknown';
431
658
  const blockedTargets = reason.match(/blockedTargets=([^;]+)/)?.[1] ?? 'none';
659
+ const supportCard = reason.match(/supportCard=([^;]+)/)?.[1] ?? 'none';
432
660
  const supportTags = reason.match(/supportTags=([^;]+)/)?.[1] ?? 'none';
433
661
  const multiplier = reason.match(/multiplier=([^;]+)/)?.[1] ?? 'unknown';
662
+ const supportSource =
663
+ mode === 'discovered-support'
664
+ ? 'discovered'
665
+ : mode === 'support'
666
+ ? 'authored'
667
+ : 'n/a';
434
668
 
435
669
  return {
436
670
  group: parsedGroup,
437
671
  mode,
672
+ supportSource,
438
673
  cardId: card.cardId,
439
674
  selected: card.selected ? 'yes' : 'no',
440
675
  finalScore: card.finalScore.toFixed(3),
441
676
  blocked,
442
677
  blockedTargets,
678
+ supportCard,
443
679
  supportTags,
444
680
  multiplier,
445
681
  };
@@ -462,6 +698,8 @@ export const pipelineDebugAPI = {
462
698
  const selectedRows = rows.filter((r) => r.selected === 'yes');
463
699
  const blockedTargetSet = new Set<string>();
464
700
  const supportTagSet = new Set<string>();
701
+ const authoredSupportSet = new Set<string>();
702
+ const discoveredSupportSet = new Set<string>();
465
703
 
466
704
  for (const row of rows) {
467
705
  if (row.blockedTargets && row.blockedTargets !== 'none') {
@@ -476,6 +714,13 @@ export const pipelineDebugAPI = {
476
714
  .filter(Boolean)
477
715
  .forEach((t) => supportTagSet.add(t));
478
716
  }
717
+ if (row.supportCard && row.supportCard !== 'none') {
718
+ if (row.supportSource === 'discovered') {
719
+ discoveredSupportSet.add(row.supportCard);
720
+ } else if (row.supportSource === 'authored') {
721
+ authoredSupportSet.add(row.supportCard);
722
+ }
723
+ }
479
724
  }
480
725
 
481
726
  logger.info(`Prescribed cards in run: ${rows.length}`);
@@ -486,6 +731,12 @@ export const pipelineDebugAPI = {
486
731
  logger.info(
487
732
  `Resolved support tags referenced: ${supportTagSet.size > 0 ? [...supportTagSet].join(', ') : 'none'}`
488
733
  );
734
+ logger.info(
735
+ `Authored support cards emitted: ${authoredSupportSet.size > 0 ? [...authoredSupportSet].join(', ') : 'none'}`
736
+ );
737
+ logger.info(
738
+ `Discovered support cards emitted: ${discoveredSupportSet.size > 0 ? [...discoveredSupportSet].join(', ') : 'none'}`
739
+ );
489
740
 
490
741
  // eslint-disable-next-line no-console
491
742
  console.groupEnd();
@@ -641,6 +892,46 @@ export const pipelineDebugAPI = {
641
892
  );
642
893
  },
643
894
 
895
+ /**
896
+ * Toggle the full-screen UI debugger.
897
+ */
898
+ ui(): void {
899
+ if (_uiContainer) {
900
+ document.body.removeChild(_uiContainer);
901
+ _uiContainer = null;
902
+ return;
903
+ }
904
+
905
+ _uiContainer = document.createElement('div');
906
+ _uiContainer.id = 'sk-pipeline-debugger';
907
+ document.body.appendChild(_uiContainer);
908
+
909
+ // Initial select last run if any
910
+ if (_selectedRunIndex === null && runHistory.length > 0) {
911
+ _selectedRunIndex = 0;
912
+ }
913
+
914
+ renderUI();
915
+ },
916
+
917
+ /**
918
+ * Internal UI helpers
919
+ * @internal
920
+ */
921
+ _selectRun(index: number): void {
922
+ _selectedRunIndex = index;
923
+ renderUI();
924
+ },
925
+
926
+ /**
927
+ * Internal UI helpers
928
+ * @internal
929
+ */
930
+ _setSearch(query: string): void {
931
+ _cardSearchQuery = query;
932
+ renderUI();
933
+ },
934
+
644
935
  /**
645
936
  * Show help.
646
937
  */
@@ -649,6 +940,7 @@ export const pipelineDebugAPI = {
649
940
  🔧 Pipeline Debug API
650
941
 
651
942
  Commands:
943
+ .ui() Toggle full-screen UI debugger
652
944
  .showLastRun() Show summary of most recent pipeline run
653
945
  .showRun(id|index) Show summary of a specific run (by index or ID suffix)
654
946
  .showCard(cardId) Show provenance trail for a specific card
@@ -665,6 +957,7 @@ Commands:
665
957
  .help() Show this help message
666
958
 
667
959
  Example:
960
+ window.skuilder.pipeline.ui()
668
961
  window.skuilder.pipeline.showLastRun()
669
962
  window.skuilder.pipeline.showRun(1)
670
963
  await window.skuilder.pipeline.diagnoseCardSpace()
@@ -223,7 +223,10 @@ export default class CompositeGenerator extends ContentNavigator implements Card
223
223
  for (const [, items] of byCardId) {
224
224
  const cards = items.map((i) => i.card);
225
225
  const aggregatedScore = this.aggregateScores(items);
226
- const finalScore = Math.min(1.0, aggregatedScore); // Clamp to [0, 1]
226
+ // Allow scores above 1.0 to pass through generators like prescribed
227
+ // intentionally use high scores to express curriculum priority.
228
+ // Only clamp negative scores.
229
+ const finalScore = Math.max(0, aggregatedScore);
227
230
 
228
231
  // Merge provenance from all generators that produced this card
229
232
  const mergedProvenance = cards.flatMap((c) => c.provenance);