@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/dist/core/index.d.cts +20 -3
- package/dist/core/index.d.ts +20 -3
- package/dist/core/index.js +461 -30
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +461 -30
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +461 -30
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +461 -30
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +457 -28
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +457 -28
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +467 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +467 -32
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/core/navigators/Pipeline.ts +104 -13
- package/src/core/navigators/PipelineDebugger.ts +296 -3
- package/src/core/navigators/generators/CompositeGenerator.ts +4 -1
- package/src/core/navigators/generators/prescribed.ts +246 -22
- package/src/impl/couch/courseDB.ts +3 -2
- package/src/study/SessionController.ts +1 -0
- package/src/study/services/CardHydrationService.ts +6 -1
- package/tests/core/navigators/CompositeGenerator.test.ts +14 -50
- package/tests/core/navigators/Pipeline.test.ts +13 -12
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.1.32-
|
|
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-
|
|
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": "^
|
|
63
|
-
"vitest": "^4.0
|
|
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 —
|
|
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
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
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:
|
|
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:
|
|
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))
|
|
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))
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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);
|