@vue-skuilder/db 0.1.23 → 0.1.25
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/{contentSource-BP9hznNV.d.ts → contentSource-BmnmvH8C.d.ts} +268 -3
- package/dist/{contentSource-DsJadoBU.d.cts → contentSource-DfBbaLA-.d.cts} +268 -3
- package/dist/core/index.d.cts +310 -6
- package/dist/core/index.d.ts +310 -6
- package/dist/core/index.js +2606 -666
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +2564 -639
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-CHYrQ5pB.d.cts → dataLayerProvider-BeRXVMs5.d.cts} +1 -1
- package/dist/{dataLayerProvider-MDTxXq2l.d.ts → dataLayerProvider-CG9GfaAY.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +11 -3
- package/dist/impl/couch/index.d.ts +11 -3
- package/dist/impl/couch/index.js +2336 -656
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2316 -631
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +4 -4
- package/dist/impl/static/index.d.ts +4 -4
- package/dist/impl/static/index.js +2312 -632
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +2315 -630
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-Dj0SEgk3.d.ts → index-BWvO-_rJ.d.ts} +1 -1
- package/dist/{index-B_j6u5E4.d.cts → index-Ba7hYbHj.d.cts} +1 -1
- package/dist/index.d.cts +278 -20
- package/dist/index.d.ts +278 -20
- package/dist/index.js +3603 -720
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3529 -674
- package/dist/index.mjs.map +1 -1
- package/dist/{types-DQaXnuoc.d.ts → types-CJrLM1Ew.d.ts} +1 -1
- package/dist/{types-Bn0itutr.d.cts → types-W8n-B6HG.d.cts} +1 -1
- package/dist/{types-legacy-DDY4N-Uq.d.cts → types-legacy-JXDxinpU.d.cts} +5 -1
- package/dist/{types-legacy-DDY4N-Uq.d.ts → types-legacy-JXDxinpU.d.ts} +5 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/docs/brainstorm-navigation-paradigm.md +40 -34
- package/docs/future-orchestration-vision.md +216 -0
- package/docs/navigators-architecture.md +210 -9
- package/docs/todo-review-urgency-adaptation.md +205 -0
- package/docs/todo-strategy-authoring.md +8 -6
- package/package.json +3 -3
- package/src/core/index.ts +2 -0
- package/src/core/interfaces/contentSource.ts +7 -0
- package/src/core/interfaces/userDB.ts +50 -0
- package/src/core/navigators/Pipeline.ts +132 -5
- package/src/core/navigators/PipelineAssembler.ts +21 -22
- package/src/core/navigators/PipelineDebugger.ts +426 -0
- package/src/core/navigators/filters/WeightedFilter.ts +141 -0
- package/src/core/navigators/filters/types.ts +4 -0
- package/src/core/navigators/generators/CompositeGenerator.ts +82 -19
- package/src/core/navigators/generators/elo.ts +14 -1
- package/src/core/navigators/generators/srs.ts +146 -18
- package/src/core/navigators/generators/types.ts +4 -0
- package/src/core/navigators/index.ts +203 -13
- package/src/core/orchestration/gradient.ts +133 -0
- package/src/core/orchestration/index.ts +210 -0
- package/src/core/orchestration/learning.ts +250 -0
- package/src/core/orchestration/recording.ts +92 -0
- package/src/core/orchestration/signal.ts +67 -0
- package/src/core/types/contentNavigationStrategy.ts +38 -0
- package/src/core/types/learningState.ts +77 -0
- package/src/core/types/types-legacy.ts +4 -0
- package/src/core/types/userOutcome.ts +51 -0
- package/src/courseConfigRegistration.ts +107 -0
- package/src/factory.ts +6 -0
- package/src/impl/common/BaseUserDB.ts +16 -0
- package/src/impl/couch/user-course-relDB.ts +12 -0
- package/src/study/MixerDebugger.ts +555 -0
- package/src/study/SessionController.ts +159 -20
- package/src/study/SessionDebugger.ts +442 -0
- package/src/study/SourceMixer.ts +36 -17
- package/src/study/TODO-session-scheduling.md +133 -0
- package/src/study/index.ts +2 -0
- package/src/study/services/EloService.ts +79 -4
- package/src/study/services/ResponseProcessor.ts +130 -72
- package/src/study/services/SrsService.ts +9 -0
- package/tests/core/navigators/Pipeline.test.ts +2 -0
- package/tests/core/navigators/PipelineAssembler.test.ts +4 -4
- package/docs/todo-evolutionary-orchestration.md +0 -310
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
import type { WeightedCard, StrategyContribution } from './index';
|
|
2
|
+
import { logger } from '../../util/logger';
|
|
3
|
+
|
|
4
|
+
// ============================================================================
|
|
5
|
+
// PIPELINE DEBUGGER
|
|
6
|
+
// ============================================================================
|
|
7
|
+
//
|
|
8
|
+
// Console-accessible debug API for inspecting pipeline decisions.
|
|
9
|
+
//
|
|
10
|
+
// Exposed as `window.skuilder.pipeline` for interactive exploration.
|
|
11
|
+
//
|
|
12
|
+
// Usage:
|
|
13
|
+
// window.skuilder.pipeline.showLastRun()
|
|
14
|
+
// window.skuilder.pipeline.showCard('cardId123')
|
|
15
|
+
// window.skuilder.pipeline.explainReviews()
|
|
16
|
+
// window.skuilder.pipeline.export()
|
|
17
|
+
//
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Summary of a single generator's contribution.
|
|
22
|
+
*/
|
|
23
|
+
export interface GeneratorSummary {
|
|
24
|
+
name: string;
|
|
25
|
+
cardCount: number;
|
|
26
|
+
newCount: number;
|
|
27
|
+
reviewCount: number;
|
|
28
|
+
topScore: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Summary of a filter's impact on scores.
|
|
33
|
+
*/
|
|
34
|
+
export interface FilterImpact {
|
|
35
|
+
name: string;
|
|
36
|
+
boosted: number;
|
|
37
|
+
penalized: number;
|
|
38
|
+
passed: number;
|
|
39
|
+
removed: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Complete record of a single pipeline execution.
|
|
44
|
+
*/
|
|
45
|
+
export interface PipelineRunReport {
|
|
46
|
+
runId: string;
|
|
47
|
+
timestamp: Date;
|
|
48
|
+
courseId: string;
|
|
49
|
+
courseName?: string;
|
|
50
|
+
|
|
51
|
+
// Generator phase
|
|
52
|
+
generatorName: string;
|
|
53
|
+
generators?: GeneratorSummary[];
|
|
54
|
+
generatedCount: number;
|
|
55
|
+
|
|
56
|
+
// Filter phase
|
|
57
|
+
filters: FilterImpact[];
|
|
58
|
+
|
|
59
|
+
// Results
|
|
60
|
+
finalCount: number;
|
|
61
|
+
reviewsSelected: number;
|
|
62
|
+
newSelected: number;
|
|
63
|
+
|
|
64
|
+
// Full card data for inspection
|
|
65
|
+
cards: Array<{
|
|
66
|
+
cardId: string;
|
|
67
|
+
courseId: string;
|
|
68
|
+
origin: 'new' | 'review' | 'unknown';
|
|
69
|
+
finalScore: number;
|
|
70
|
+
provenance: StrategyContribution[];
|
|
71
|
+
selected: boolean;
|
|
72
|
+
}>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Ring buffer for storing recent pipeline runs.
|
|
77
|
+
*/
|
|
78
|
+
const MAX_RUNS = 10;
|
|
79
|
+
const runHistory: PipelineRunReport[] = [];
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Determine card origin from provenance trail.
|
|
83
|
+
*/
|
|
84
|
+
function getOrigin(card: WeightedCard): 'new' | 'review' | 'unknown' {
|
|
85
|
+
const firstEntry = card.provenance[0];
|
|
86
|
+
if (!firstEntry) return 'unknown';
|
|
87
|
+
const reason = firstEntry.reason?.toLowerCase() || '';
|
|
88
|
+
if (reason.includes('new card')) return 'new';
|
|
89
|
+
if (reason.includes('review')) return 'review';
|
|
90
|
+
return 'unknown';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Capture a pipeline run for later inspection.
|
|
95
|
+
*/
|
|
96
|
+
export function captureRun(report: Omit<PipelineRunReport, 'runId' | 'timestamp'>): void {
|
|
97
|
+
const fullReport: PipelineRunReport = {
|
|
98
|
+
...report,
|
|
99
|
+
runId: `run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
100
|
+
timestamp: new Date(),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
runHistory.unshift(fullReport);
|
|
104
|
+
if (runHistory.length > MAX_RUNS) {
|
|
105
|
+
runHistory.pop();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Build a capture-ready report from pipeline execution data.
|
|
111
|
+
*/
|
|
112
|
+
export function buildRunReport(
|
|
113
|
+
courseId: string,
|
|
114
|
+
courseName: string | undefined,
|
|
115
|
+
generatorName: string,
|
|
116
|
+
generators: GeneratorSummary[] | undefined,
|
|
117
|
+
generatedCount: number,
|
|
118
|
+
filters: FilterImpact[],
|
|
119
|
+
allCards: WeightedCard[],
|
|
120
|
+
selectedCards: WeightedCard[]
|
|
121
|
+
): Omit<PipelineRunReport, 'runId' | 'timestamp'> {
|
|
122
|
+
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
123
|
+
|
|
124
|
+
const cards = allCards.map((card) => ({
|
|
125
|
+
cardId: card.cardId,
|
|
126
|
+
courseId: card.courseId,
|
|
127
|
+
origin: getOrigin(card),
|
|
128
|
+
finalScore: card.score,
|
|
129
|
+
provenance: card.provenance,
|
|
130
|
+
selected: selectedIds.has(card.cardId),
|
|
131
|
+
}));
|
|
132
|
+
|
|
133
|
+
const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === 'review').length;
|
|
134
|
+
const newSelected = selectedCards.filter((c) => getOrigin(c) === 'new').length;
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
courseId,
|
|
138
|
+
courseName,
|
|
139
|
+
generatorName,
|
|
140
|
+
generators,
|
|
141
|
+
generatedCount,
|
|
142
|
+
filters,
|
|
143
|
+
finalCount: selectedCards.length,
|
|
144
|
+
reviewsSelected,
|
|
145
|
+
newSelected,
|
|
146
|
+
cards,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// CONSOLE API
|
|
152
|
+
// ============================================================================
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Format a provenance trail for console display.
|
|
156
|
+
*/
|
|
157
|
+
function formatProvenance(provenance: StrategyContribution[]): string {
|
|
158
|
+
return provenance
|
|
159
|
+
.map((p) => {
|
|
160
|
+
const actionSymbol =
|
|
161
|
+
p.action === 'generated'
|
|
162
|
+
? '🎲'
|
|
163
|
+
: p.action === 'boosted'
|
|
164
|
+
? '⬆️'
|
|
165
|
+
: p.action === 'penalized'
|
|
166
|
+
? '⬇️'
|
|
167
|
+
: '➡️';
|
|
168
|
+
return ` ${actionSymbol} ${p.strategyName}: ${p.score.toFixed(3)} - ${p.reason}`;
|
|
169
|
+
})
|
|
170
|
+
.join('\n');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Print summary of a single pipeline run.
|
|
175
|
+
*/
|
|
176
|
+
function printRunSummary(run: PipelineRunReport): void {
|
|
177
|
+
// eslint-disable-next-line no-console
|
|
178
|
+
console.group(`🔍 Pipeline Run: ${run.courseId} (${run.courseName || 'unnamed'})`);
|
|
179
|
+
logger.info(`Run ID: ${run.runId}`);
|
|
180
|
+
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
181
|
+
logger.info(`Generator: ${run.generatorName} → ${run.generatedCount} candidates`);
|
|
182
|
+
|
|
183
|
+
if (run.generators && run.generators.length > 0) {
|
|
184
|
+
// eslint-disable-next-line no-console
|
|
185
|
+
console.group('Generator breakdown:');
|
|
186
|
+
for (const g of run.generators) {
|
|
187
|
+
logger.info(
|
|
188
|
+
` ${g.name}: ${g.cardCount} cards (${g.newCount} new, ${g.reviewCount} reviews, top: ${g.topScore.toFixed(2)})`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
// eslint-disable-next-line no-console
|
|
192
|
+
console.groupEnd();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (run.filters.length > 0) {
|
|
196
|
+
// eslint-disable-next-line no-console
|
|
197
|
+
console.group('Filter impact:');
|
|
198
|
+
for (const f of run.filters) {
|
|
199
|
+
logger.info(` ${f.name}: ↑${f.boosted} ↓${f.penalized} =${f.passed} ✕${f.removed}`);
|
|
200
|
+
}
|
|
201
|
+
// eslint-disable-next-line no-console
|
|
202
|
+
console.groupEnd();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
logger.info(
|
|
206
|
+
`Result: ${run.finalCount} cards selected (${run.newSelected} new, ${run.reviewsSelected} reviews)`
|
|
207
|
+
);
|
|
208
|
+
// eslint-disable-next-line no-console
|
|
209
|
+
console.groupEnd();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Console API object exposed on window.skuilder.pipeline
|
|
214
|
+
*/
|
|
215
|
+
export const pipelineDebugAPI = {
|
|
216
|
+
/**
|
|
217
|
+
* Get raw run history for programmatic access.
|
|
218
|
+
*/
|
|
219
|
+
get runs(): PipelineRunReport[] {
|
|
220
|
+
return [...runHistory];
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Show summary of a specific pipeline run.
|
|
225
|
+
*/
|
|
226
|
+
showRun(idOrIndex: string | number = 0): void {
|
|
227
|
+
if (runHistory.length === 0) {
|
|
228
|
+
logger.info('[Pipeline Debug] No runs captured yet.');
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let run: PipelineRunReport | undefined;
|
|
233
|
+
|
|
234
|
+
if (typeof idOrIndex === 'number') {
|
|
235
|
+
run = runHistory[idOrIndex];
|
|
236
|
+
if (!run) {
|
|
237
|
+
logger.info(
|
|
238
|
+
`[Pipeline Debug] No run found at index ${idOrIndex}. History length: ${runHistory.length}`
|
|
239
|
+
);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
} else {
|
|
243
|
+
run = runHistory.find((r) => r.runId.endsWith(idOrIndex));
|
|
244
|
+
if (!run) {
|
|
245
|
+
logger.info(`[Pipeline Debug] No run found matching ID '${idOrIndex}'.`);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
printRunSummary(run);
|
|
251
|
+
},
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Show summary of the last pipeline run.
|
|
255
|
+
*/
|
|
256
|
+
showLastRun(): void {
|
|
257
|
+
this.showRun(0);
|
|
258
|
+
},
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Show detailed provenance for a specific card.
|
|
262
|
+
*/
|
|
263
|
+
showCard(cardId: string): void {
|
|
264
|
+
for (const run of runHistory) {
|
|
265
|
+
const card = run.cards.find((c) => c.cardId === cardId);
|
|
266
|
+
if (card) {
|
|
267
|
+
// eslint-disable-next-line no-console
|
|
268
|
+
console.group(`🎴 Card: ${cardId}`);
|
|
269
|
+
logger.info(`Course: ${card.courseId}`);
|
|
270
|
+
logger.info(`Origin: ${card.origin}`);
|
|
271
|
+
logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
|
|
272
|
+
logger.info(`Selected: ${card.selected ? 'Yes ✅' : 'No ❌'}`);
|
|
273
|
+
logger.info('Provenance:');
|
|
274
|
+
logger.info(formatProvenance(card.provenance));
|
|
275
|
+
// eslint-disable-next-line no-console
|
|
276
|
+
console.groupEnd();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
logger.info(`[Pipeline Debug] Card '${cardId}' not found in recent runs.`);
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Explain why reviews may or may not have been selected.
|
|
285
|
+
*/
|
|
286
|
+
explainReviews(): void {
|
|
287
|
+
if (runHistory.length === 0) {
|
|
288
|
+
logger.info('[Pipeline Debug] No runs captured yet.');
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// eslint-disable-next-line no-console
|
|
293
|
+
console.group('📋 Review Selection Analysis');
|
|
294
|
+
|
|
295
|
+
for (const run of runHistory) {
|
|
296
|
+
// eslint-disable-next-line no-console
|
|
297
|
+
console.group(`Run: ${run.courseId} @ ${run.timestamp.toLocaleTimeString()}`);
|
|
298
|
+
|
|
299
|
+
const allReviews = run.cards.filter((c) => c.origin === 'review');
|
|
300
|
+
const selectedReviews = allReviews.filter((c) => c.selected);
|
|
301
|
+
|
|
302
|
+
if (allReviews.length === 0) {
|
|
303
|
+
logger.info('❌ No reviews were generated. Check SRS logs for why.');
|
|
304
|
+
} else if (selectedReviews.length === 0) {
|
|
305
|
+
logger.info(`⚠️ ${allReviews.length} reviews generated but none selected.`);
|
|
306
|
+
logger.info('Possible reasons:');
|
|
307
|
+
|
|
308
|
+
// Check if new cards scored higher
|
|
309
|
+
const topNewScore = Math.max(
|
|
310
|
+
...run.cards.filter((c) => c.origin === 'new' && c.selected).map((c) => c.finalScore),
|
|
311
|
+
0
|
|
312
|
+
);
|
|
313
|
+
const topReviewScore = Math.max(...allReviews.map((c) => c.finalScore), 0);
|
|
314
|
+
|
|
315
|
+
if (topReviewScore < topNewScore) {
|
|
316
|
+
logger.info(
|
|
317
|
+
` - New cards scored higher (top new: ${topNewScore.toFixed(2)}, top review: ${topReviewScore.toFixed(2)})`
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Show top review that didn't make it
|
|
322
|
+
const topReview = allReviews.sort((a, b) => b.finalScore - a.finalScore)[0];
|
|
323
|
+
if (topReview) {
|
|
324
|
+
logger.info(` - Top review score: ${topReview.finalScore.toFixed(3)}`);
|
|
325
|
+
logger.info(' - Its provenance:');
|
|
326
|
+
logger.info(formatProvenance(topReview.provenance));
|
|
327
|
+
}
|
|
328
|
+
} else {
|
|
329
|
+
logger.info(`✅ ${selectedReviews.length}/${allReviews.length} reviews selected.`);
|
|
330
|
+
logger.info('Top selected review:');
|
|
331
|
+
const topSelected = selectedReviews.sort((a, b) => b.finalScore - a.finalScore)[0];
|
|
332
|
+
logger.info(formatProvenance(topSelected.provenance));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// eslint-disable-next-line no-console
|
|
336
|
+
console.groupEnd();
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// eslint-disable-next-line no-console
|
|
340
|
+
console.groupEnd();
|
|
341
|
+
},
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Show all runs in compact format.
|
|
345
|
+
*/
|
|
346
|
+
listRuns(): void {
|
|
347
|
+
if (runHistory.length === 0) {
|
|
348
|
+
logger.info('[Pipeline Debug] No runs captured yet.');
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// eslint-disable-next-line no-console
|
|
353
|
+
console.table(
|
|
354
|
+
runHistory.map((r) => ({
|
|
355
|
+
id: r.runId.slice(-8),
|
|
356
|
+
time: r.timestamp.toLocaleTimeString(),
|
|
357
|
+
course: r.courseName || r.courseId.slice(0, 8),
|
|
358
|
+
generated: r.generatedCount,
|
|
359
|
+
selected: r.finalCount,
|
|
360
|
+
new: r.newSelected,
|
|
361
|
+
reviews: r.reviewsSelected,
|
|
362
|
+
}))
|
|
363
|
+
);
|
|
364
|
+
},
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Export run history as JSON for bug reports.
|
|
368
|
+
*/
|
|
369
|
+
export(): string {
|
|
370
|
+
const json = JSON.stringify(runHistory, null, 2);
|
|
371
|
+
logger.info('[Pipeline Debug] Run history exported. Copy the returned string or use:');
|
|
372
|
+
logger.info(' copy(window.skuilder.pipeline.export())');
|
|
373
|
+
return json;
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Clear run history.
|
|
378
|
+
*/
|
|
379
|
+
clear(): void {
|
|
380
|
+
runHistory.length = 0;
|
|
381
|
+
logger.info('[Pipeline Debug] Run history cleared.');
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Show help.
|
|
386
|
+
*/
|
|
387
|
+
help(): void {
|
|
388
|
+
logger.info(`
|
|
389
|
+
🔧 Pipeline Debug API
|
|
390
|
+
|
|
391
|
+
Commands:
|
|
392
|
+
.showLastRun() Show summary of most recent pipeline run
|
|
393
|
+
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
394
|
+
.showCard(cardId) Show provenance trail for a specific card
|
|
395
|
+
.explainReviews() Analyze why reviews were/weren't selected
|
|
396
|
+
.listRuns() List all captured runs in table format
|
|
397
|
+
.export() Export run history as JSON for bug reports
|
|
398
|
+
.clear() Clear run history
|
|
399
|
+
.runs Access raw run history array
|
|
400
|
+
.help() Show this help message
|
|
401
|
+
|
|
402
|
+
Example:
|
|
403
|
+
window.skuilder.pipeline.showLastRun()
|
|
404
|
+
window.skuilder.pipeline.showRun(1)
|
|
405
|
+
window.skuilder.pipeline.showCard('abc123')
|
|
406
|
+
`);
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// ============================================================================
|
|
411
|
+
// WINDOW MOUNT
|
|
412
|
+
// ============================================================================
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Mount the debug API on window.skuilder.pipeline
|
|
416
|
+
*/
|
|
417
|
+
export function mountPipelineDebugger(): void {
|
|
418
|
+
if (typeof window === 'undefined') return;
|
|
419
|
+
|
|
420
|
+
const win = window as any;
|
|
421
|
+
win.skuilder = win.skuilder || {};
|
|
422
|
+
win.skuilder.pipeline = pipelineDebugAPI;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Auto-mount when module is loaded
|
|
426
|
+
mountPipelineDebugger();
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { CardFilter, FilterContext } from './types';
|
|
2
|
+
import { WeightedCard } from '../index';
|
|
3
|
+
import { LearnableWeight, DEFAULT_LEARNABLE_WEIGHT } from '../../types/contentNavigationStrategy';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wraps a CardFilter to apply evolutionary weighting to its effects.
|
|
7
|
+
*
|
|
8
|
+
* If a filter applies a multiplier M (score * M), and the strategy has
|
|
9
|
+
* an effective weight W, the final multiplier becomes M^W.
|
|
10
|
+
*
|
|
11
|
+
* - W=1.0: Original behavior
|
|
12
|
+
* - W>1.0: Amplifies the filter's opinion (stronger boost/penalty)
|
|
13
|
+
* - W<1.0: Dampens the filter's opinion (weaker boost/penalty)
|
|
14
|
+
* - W=0.0: Nullifies the filter (identity)
|
|
15
|
+
*
|
|
16
|
+
* This wrapper handles the math of scaling the filter's impact and updating
|
|
17
|
+
* the provenance trail with the effective weight used.
|
|
18
|
+
*/
|
|
19
|
+
export class WeightedFilter implements CardFilter {
|
|
20
|
+
public name: string;
|
|
21
|
+
private inner: CardFilter;
|
|
22
|
+
private learnable: LearnableWeight;
|
|
23
|
+
private staticWeight: boolean;
|
|
24
|
+
private strategyId?: string;
|
|
25
|
+
|
|
26
|
+
constructor(
|
|
27
|
+
inner: CardFilter,
|
|
28
|
+
learnable: LearnableWeight = DEFAULT_LEARNABLE_WEIGHT,
|
|
29
|
+
staticWeight: boolean = false,
|
|
30
|
+
strategyId?: string
|
|
31
|
+
) {
|
|
32
|
+
this.inner = inner;
|
|
33
|
+
this.name = inner.name;
|
|
34
|
+
this.learnable = learnable;
|
|
35
|
+
this.staticWeight = staticWeight;
|
|
36
|
+
this.strategyId = strategyId;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Apply the inner filter, then scale its effect by the configured weight.
|
|
41
|
+
*/
|
|
42
|
+
async transform(cards: WeightedCard[], context: FilterContext): Promise<WeightedCard[]> {
|
|
43
|
+
// ========================================================================
|
|
44
|
+
// 1. DETERMINE EFFECTIVE WEIGHT
|
|
45
|
+
// ========================================================================
|
|
46
|
+
|
|
47
|
+
// Determine effective weight using orchestration context if available
|
|
48
|
+
let effectiveWeight = this.learnable.weight;
|
|
49
|
+
let deviation: number | undefined;
|
|
50
|
+
|
|
51
|
+
if (!this.staticWeight && context.orchestration) {
|
|
52
|
+
// ContentNavigator instances have a strategyId property (protected/private)
|
|
53
|
+
// We assume inner filter is a ContentNavigator or has a strategyId property.
|
|
54
|
+
// Fallback to name if not present.
|
|
55
|
+
const strategyId = this.strategyId || (this.inner as any).strategyId || this.name;
|
|
56
|
+
effectiveWeight = context.orchestration.getEffectiveWeight(strategyId, this.learnable);
|
|
57
|
+
deviation = context.orchestration.getDeviation(strategyId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Optimization: If weight is 1.0, the scaling is an identity operation.
|
|
61
|
+
// Just run the inner filter directly.
|
|
62
|
+
if (Math.abs(effectiveWeight - 1.0) < 0.001) {
|
|
63
|
+
return this.inner.transform(cards, context);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ========================================================================
|
|
67
|
+
// 2. CAPTURE STATE BEFORE FILTER
|
|
68
|
+
// ========================================================================
|
|
69
|
+
|
|
70
|
+
// We need original scores to calculate what the filter did (M = new/old)
|
|
71
|
+
const originalScores = new Map<string, number>();
|
|
72
|
+
for (const card of cards) {
|
|
73
|
+
originalScores.set(card.cardId, card.score);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ========================================================================
|
|
77
|
+
// 3. RUN INNER FILTER
|
|
78
|
+
// ========================================================================
|
|
79
|
+
|
|
80
|
+
const transformedCards = await this.inner.transform(cards, context);
|
|
81
|
+
|
|
82
|
+
// ========================================================================
|
|
83
|
+
// 4. APPLY WEIGHT SCALING
|
|
84
|
+
// ========================================================================
|
|
85
|
+
|
|
86
|
+
return transformedCards.map((card) => {
|
|
87
|
+
const originalScore = originalScores.get(card.cardId);
|
|
88
|
+
|
|
89
|
+
// Edge cases where we can't or shouldn't scale:
|
|
90
|
+
// - Original score missing (shouldn't happen)
|
|
91
|
+
// - Original score 0 (was already excluded)
|
|
92
|
+
// - New score 0 (filter excluded it / vetoed) - we treat vetoes as absolute
|
|
93
|
+
if (originalScore === undefined || originalScore === 0 || card.score === 0) {
|
|
94
|
+
return card;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Calculate raw effect multiplier: M = new / old
|
|
98
|
+
const rawEffect = card.score / originalScore;
|
|
99
|
+
|
|
100
|
+
// If filter didn't change this card, nothing to scale
|
|
101
|
+
if (Math.abs(rawEffect - 1.0) < 0.0001) {
|
|
102
|
+
return card;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Apply weight: scaled = M ^ W
|
|
106
|
+
// Example: 0.5 penalty ^ 2.0 weight = 0.25 (stronger penalty)
|
|
107
|
+
// Example: 0.5 penalty ^ 0.5 weight = 0.707 (weaker penalty)
|
|
108
|
+
const weightedEffect = Math.pow(rawEffect, effectiveWeight);
|
|
109
|
+
const newScore = originalScore * weightedEffect;
|
|
110
|
+
|
|
111
|
+
// Update provenance
|
|
112
|
+
// The inner filter just added the last entry. We need to update it
|
|
113
|
+
// to reflect the weighted score and record the effective weight.
|
|
114
|
+
const lastProvIndex = card.provenance.length - 1;
|
|
115
|
+
const lastProv = card.provenance[lastProvIndex];
|
|
116
|
+
|
|
117
|
+
if (lastProv) {
|
|
118
|
+
const updatedProvenance = [...card.provenance];
|
|
119
|
+
updatedProvenance[lastProvIndex] = {
|
|
120
|
+
...lastProv,
|
|
121
|
+
score: newScore,
|
|
122
|
+
effectiveWeight: effectiveWeight,
|
|
123
|
+
deviation: deviation,
|
|
124
|
+
// We can optionally append to the reason, but the structured field is key
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
...card,
|
|
129
|
+
score: newScore,
|
|
130
|
+
provenance: updatedProvenance,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Fallback if no provenance found (rare)
|
|
135
|
+
return {
|
|
136
|
+
...card,
|
|
137
|
+
score: newScore,
|
|
138
|
+
};
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WeightedCard } from '../index';
|
|
2
2
|
import type { CourseDBInterface } from '../../interfaces/courseDB';
|
|
3
3
|
import type { UserDBInterface } from '../../interfaces/userDB';
|
|
4
|
+
import type { OrchestrationContext } from '../../orchestration';
|
|
4
5
|
|
|
5
6
|
// ============================================================================
|
|
6
7
|
// CARD FILTER INTERFACE
|
|
@@ -40,6 +41,9 @@ export interface FilterContext {
|
|
|
40
41
|
/** User's global ELO score for this course */
|
|
41
42
|
userElo: number;
|
|
42
43
|
|
|
44
|
+
/** Orchestration context for evolutionary weighting */
|
|
45
|
+
orchestration?: OrchestrationContext;
|
|
46
|
+
|
|
43
47
|
// Future extensions:
|
|
44
48
|
// - hydrated tags for all cards (batch lookup)
|
|
45
49
|
// - user's tag-level ELO data
|