@vue-skuilder/db 0.1.32-a → 0.1.32-c
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 +16 -12
- package/dist/core/index.d.ts +16 -12
- package/dist/core/index.js +2279 -227
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +2256 -200
- package/dist/core/index.mjs.map +1 -1
- package/dist/{contentSource-Bdwkvqa8.d.ts → dataLayerProvider-BAn-LRh5.d.ts} +626 -83
- package/dist/{contentSource-DF1nUbPQ.d.cts → dataLayerProvider-BJqBlMIl.d.cts} +626 -83
- package/dist/impl/couch/index.d.cts +18 -3
- package/dist/impl/couch/index.d.ts +18 -3
- package/dist/impl/couch/index.js +2323 -224
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2311 -208
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +5 -4
- package/dist/impl/static/index.d.ts +5 -4
- package/dist/impl/static/index.js +2283 -231
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +2268 -212
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-BWvO-_rJ.d.ts → index-X6wHrURm.d.ts} +1 -1
- package/dist/{index-Ba7hYbHj.d.cts → index-m8MMGxxR.d.cts} +1 -1
- package/dist/index.d.cts +9 -381
- package/dist/index.d.ts +9 -381
- package/dist/index.js +9626 -8815
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9559 -8748
- package/dist/index.mjs.map +1 -1
- package/dist/{types-CJrLM1Ew.d.ts → types-DZ5dUqbL.d.ts} +1 -1
- package/dist/{types-W8n-B6HG.d.cts → types-ZL8tOPQZ.d.cts} +1 -1
- package/dist/{types-legacy-JXDxinpU.d.cts → types-legacy-C7r0T4OV.d.cts} +1 -1
- package/dist/{types-legacy-JXDxinpU.d.ts → types-legacy-C7r0T4OV.d.ts} +1 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/docs/navigators-architecture.md +2 -2
- package/package.json +2 -2
- package/src/core/interfaces/contentSource.ts +2 -1
- package/src/core/navigators/Pipeline.ts +51 -25
- package/src/core/navigators/PipelineDebugger.ts +49 -1
- package/src/core/navigators/filters/hierarchyDefinition.ts +92 -5
- package/src/core/navigators/filters/relativePriority.ts +7 -1
- package/src/core/navigators/generators/prescribed.ts +618 -43
- package/src/core/navigators/index.ts +2 -1
- package/src/impl/couch/CourseSyncService.ts +72 -4
- package/src/impl/couch/courseDB.ts +11 -0
- package/src/impl/static/courseDB.ts +13 -0
- package/src/study/SessionController.ts +276 -24
- package/src/study/services/EloService.ts +22 -3
- package/src/study/services/ResponseProcessor.ts +7 -3
- package/dist/dataLayerProvider-BKmVoyJR.d.ts +0 -67
- package/dist/dataLayerProvider-BQdfJuBN.d.cts +0 -67
|
@@ -157,4 +157,4 @@ interface QuestionRecord extends CardRecord, Evaluation {
|
|
|
157
157
|
priorAttemps: number;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
export { type
|
|
160
|
+
export { type CardData as C, DocType as D, type Field as F, GuestUsername as G, type QualifiedCardID as Q, type SkuilderCourseData as S, type TagStub as T, type Tag as a, type CourseListData as b, type DisplayableData as c, type DataShapeData as d, type QuestionData as e, DocTypePrefixes as f, type CardHistory as g, type CardRecord as h, type QuestionRecord as i, log as l };
|
|
@@ -157,4 +157,4 @@ interface QuestionRecord extends CardRecord, Evaluation {
|
|
|
157
157
|
priorAttemps: number;
|
|
158
158
|
}
|
|
159
159
|
|
|
160
|
-
export { type
|
|
160
|
+
export { type CardData as C, DocType as D, type Field as F, GuestUsername as G, type QualifiedCardID as Q, type SkuilderCourseData as S, type TagStub as T, type Tag as a, type CourseListData as b, type DisplayableData as c, type DataShapeData as d, type QuestionData as e, DocTypePrefixes as f, type CardHistory as g, type CardRecord as h, type QuestionRecord as i, log as l };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-
|
|
2
|
-
export { C as CouchDBToStaticPacker } from '../../index-
|
|
1
|
+
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-ZL8tOPQZ.cjs';
|
|
2
|
+
export { C as CouchDBToStaticPacker } from '../../index-m8MMGxxR.cjs';
|
|
3
3
|
import '@vue-skuilder/common';
|
|
4
|
-
import '../../types-legacy-
|
|
4
|
+
import '../../types-legacy-C7r0T4OV.cjs';
|
|
5
5
|
import 'moment';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-
|
|
2
|
-
export { C as CouchDBToStaticPacker } from '../../index-
|
|
1
|
+
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-DZ5dUqbL.js';
|
|
2
|
+
export { C as CouchDBToStaticPacker } from '../../index-X6wHrURm.js';
|
|
3
3
|
import '@vue-skuilder/common';
|
|
4
|
-
import '../../types-legacy-
|
|
4
|
+
import '../../types-legacy-C7r0T4OV.js';
|
|
5
5
|
import 'moment';
|
|
@@ -48,7 +48,7 @@ interface CardGenerator {
|
|
|
48
48
|
**Implementations:**
|
|
49
49
|
- `ELONavigator` — New cards scored by ELO proximity to user skill (scores 0.0-1.0)
|
|
50
50
|
- `SRSNavigator` — Review cards scored by overdueness, interval recency, and **backlog pressure** (scores 0.5-1.0)
|
|
51
|
-
- `HardcodedOrderNavigator` — Fixed sequence defined by course author
|
|
51
|
+
- `HardcodedOrderNavigator` — Fixed sequence defined by course author // NB this no longer exists but /home/colin/pn/vue-skuilder/master/packages/db/src/core/navigators/generators/prescribed.ts does - please update the doc in place
|
|
52
52
|
- `CompositeGenerator` — Merges multiple generators with frequency boost
|
|
53
53
|
|
|
54
54
|
#### SRS Backlog Pressure
|
|
@@ -620,4 +620,4 @@ return { ...card, score: card.score * multiplier };
|
|
|
620
620
|
- `todo-review-adaptation.md` — Planned per-user review urgency adaptation
|
|
621
621
|
- `future-orchestration-vision.md` — Long-term adaptive strategy vision (beyond current implementation)
|
|
622
622
|
- `devlog/1004` — Implementation details for tag hydration optimization
|
|
623
|
-
- `devlog/1032-orchestrator` — Evolutionary orchestration implementation details
|
|
623
|
+
- `devlog/1032-orchestrator` — Evolutionary orchestration implementation details
|
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-c",
|
|
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-c",
|
|
52
52
|
"cross-fetch": "^4.1.0",
|
|
53
53
|
"moment": "^2.29.4",
|
|
54
54
|
"pouchdb": "^9.0.0",
|
|
@@ -5,6 +5,7 @@ import { WeightedCard } from '../navigators';
|
|
|
5
5
|
import { TagFilter, hasActiveFilter } from '@vue-skuilder/common';
|
|
6
6
|
import { TagFilteredContentSource } from '../../study/TagFilteredContentSource';
|
|
7
7
|
import { OrchestrationContext } from '../orchestration';
|
|
8
|
+
import type { ReplanHints } from '@db/study/SessionController';
|
|
8
9
|
|
|
9
10
|
export type StudySessionFailedItem = StudySessionFailedNewItem | StudySessionFailedReviewItem;
|
|
10
11
|
|
|
@@ -84,7 +85,7 @@ export interface StudyContentSource {
|
|
|
84
85
|
* Set ephemeral hints for the next pipeline run.
|
|
85
86
|
* No-op for sources that don't support hints.
|
|
86
87
|
*/
|
|
87
|
-
setEphemeralHints?(hints:
|
|
88
|
+
setEphemeralHints?(hints: ReplanHints): void;
|
|
88
89
|
}
|
|
89
90
|
// #endregion docs_StudyContentSource
|
|
90
91
|
|
|
@@ -22,24 +22,9 @@ import { captureRun, buildRunReport, registerPipelineForDebug, type GeneratorSum
|
|
|
22
22
|
// 'gpc:exercise:t-*' — all t-variant exercises
|
|
23
23
|
//
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
*/
|
|
29
|
-
export interface ReplanHints {
|
|
30
|
-
/** Multiply scores for cards matching these tag patterns. */
|
|
31
|
-
boostTags?: Record<string, number>;
|
|
32
|
-
/** Multiply scores for these specific card IDs (glob patterns). */
|
|
33
|
-
boostCards?: Record<string, number>;
|
|
34
|
-
/** Cards matching these tag patterns MUST appear in results. */
|
|
35
|
-
requireTags?: string[];
|
|
36
|
-
/** These specific card IDs MUST appear in results. */
|
|
37
|
-
requireCards?: string[];
|
|
38
|
-
/** Remove cards matching these tag patterns from results. */
|
|
39
|
-
excludeTags?: string[];
|
|
40
|
-
/** Remove these specific card IDs from results. */
|
|
41
|
-
excludeCards?: string[];
|
|
42
|
-
}
|
|
25
|
+
// ReplanHints is the canonical type — re-export for consumers that import from Pipeline
|
|
26
|
+
import { ReplanHints } from '@db/study/SessionController';
|
|
27
|
+
export { ReplanHints };
|
|
43
28
|
|
|
44
29
|
/**
|
|
45
30
|
* Convert a glob pattern (with `*` wildcards) to a RegExp.
|
|
@@ -289,8 +274,8 @@ export class Pipeline extends ContentNavigator {
|
|
|
289
274
|
*
|
|
290
275
|
* Overrides ContentNavigator.setEphemeralHints() no-op.
|
|
291
276
|
*/
|
|
292
|
-
override setEphemeralHints(hints:
|
|
293
|
-
this._ephemeralHints = hints
|
|
277
|
+
override setEphemeralHints(hints: ReplanHints): void {
|
|
278
|
+
this._ephemeralHints = hints;
|
|
294
279
|
logger.info(`[Pipeline] Ephemeral hints set: ${JSON.stringify(hints)}`);
|
|
295
280
|
}
|
|
296
281
|
|
|
@@ -432,6 +417,9 @@ export class Pipeline extends ContentNavigator {
|
|
|
432
417
|
// Capture run for debug API
|
|
433
418
|
try {
|
|
434
419
|
const courseName = await this.course?.getCourseConfig().then((c) => c.name).catch(() => undefined);
|
|
420
|
+
// Use the full post-filter sorted array (not just top N) so that
|
|
421
|
+
// showCard() can inspect provenance for cards that didn't make the cut.
|
|
422
|
+
// `cards` is the post-filter, post-hints, sorted array.
|
|
435
423
|
const report = buildRunReport(
|
|
436
424
|
this.course?.getCourseID() || 'unknown',
|
|
437
425
|
courseName,
|
|
@@ -439,8 +427,9 @@ export class Pipeline extends ContentNavigator {
|
|
|
439
427
|
generatorSummaries,
|
|
440
428
|
generatedCount,
|
|
441
429
|
filterImpacts,
|
|
442
|
-
|
|
443
|
-
result
|
|
430
|
+
cards,
|
|
431
|
+
result,
|
|
432
|
+
context.userElo
|
|
444
433
|
);
|
|
445
434
|
captureRun(report);
|
|
446
435
|
} catch (e) {
|
|
@@ -544,7 +533,7 @@ export class Pipeline extends ContentNavigator {
|
|
|
544
533
|
card.provenance.push({
|
|
545
534
|
strategy: 'ephemeralHint',
|
|
546
535
|
strategyId: 'ephemeral-hint',
|
|
547
|
-
strategyName: 'Replan Hint',
|
|
536
|
+
strategyName: hints._label ? `Replan Hint (${hints._label})` : 'Replan Hint',
|
|
548
537
|
action: 'boosted',
|
|
549
538
|
score: card.score,
|
|
550
539
|
reason: `boostTag ${pattern} ×${factor}`,
|
|
@@ -561,7 +550,7 @@ export class Pipeline extends ContentNavigator {
|
|
|
561
550
|
card.provenance.push({
|
|
562
551
|
strategy: 'ephemeralHint',
|
|
563
552
|
strategyId: 'ephemeral-hint',
|
|
564
|
-
strategyName: 'Replan Hint',
|
|
553
|
+
strategyName: hints._label ? `Replan Hint (${hints._label})` : 'Replan Hint',
|
|
565
554
|
action: 'boosted',
|
|
566
555
|
score: card.score,
|
|
567
556
|
reason: `boostCard ${pattern} ×${factor}`,
|
|
@@ -573,6 +562,7 @@ export class Pipeline extends ContentNavigator {
|
|
|
573
562
|
|
|
574
563
|
// 3. Require — inject from the full pool if not already present
|
|
575
564
|
const cardIds = new Set(cards.map((c) => c.cardId));
|
|
565
|
+
const hintLabel = hints._label ? `Replan Hint (${hints._label})` : 'Replan Hint';
|
|
576
566
|
const inject = (card: WeightedCard, reason: string) => {
|
|
577
567
|
if (!cardIds.has(card.cardId)) {
|
|
578
568
|
// Give required cards a floor score so they sort above zero-score filler
|
|
@@ -585,7 +575,7 @@ export class Pipeline extends ContentNavigator {
|
|
|
585
575
|
{
|
|
586
576
|
strategy: 'ephemeralHint',
|
|
587
577
|
strategyId: 'ephemeral-hint',
|
|
588
|
-
strategyName:
|
|
578
|
+
strategyName: hintLabel,
|
|
589
579
|
action: 'boosted',
|
|
590
580
|
score: floorScore,
|
|
591
581
|
reason,
|
|
@@ -699,6 +689,42 @@ export class Pipeline extends ContentNavigator {
|
|
|
699
689
|
return [...new Set(ids)];
|
|
700
690
|
}
|
|
701
691
|
|
|
692
|
+
// ---------------------------------------------------------------------------
|
|
693
|
+
// Tag ELO diagnostic
|
|
694
|
+
// ---------------------------------------------------------------------------
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* Get the user's per-tag ELO data for specified tags (or all tags).
|
|
698
|
+
* Useful for diagnosing why hierarchy gates are open/closed.
|
|
699
|
+
*/
|
|
700
|
+
async getTagEloStatus(
|
|
701
|
+
tagFilter?: string | string[]
|
|
702
|
+
): Promise<Record<string, { score: number; count: number }>> {
|
|
703
|
+
const courseReg = await this.user!.getCourseRegDoc(this.course!.getCourseID());
|
|
704
|
+
const courseElo = toCourseElo(courseReg.elo);
|
|
705
|
+
|
|
706
|
+
const result: Record<string, { score: number; count: number }> = {};
|
|
707
|
+
|
|
708
|
+
if (!tagFilter) {
|
|
709
|
+
// Return all tags
|
|
710
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
711
|
+
result[tag] = { score: data.score, count: data.count };
|
|
712
|
+
}
|
|
713
|
+
} else {
|
|
714
|
+
const patterns = Array.isArray(tagFilter) ? tagFilter : [tagFilter];
|
|
715
|
+
for (const pattern of patterns) {
|
|
716
|
+
const regex = globToRegex(pattern);
|
|
717
|
+
for (const [tag, data] of Object.entries(courseElo.tags)) {
|
|
718
|
+
if (regex.test(tag)) {
|
|
719
|
+
result[tag] = { score: data.score, count: data.count };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return result;
|
|
726
|
+
}
|
|
727
|
+
|
|
702
728
|
// ---------------------------------------------------------------------------
|
|
703
729
|
// Card-space diagnostic
|
|
704
730
|
// ---------------------------------------------------------------------------
|
|
@@ -71,6 +71,9 @@ export interface PipelineRunReport {
|
|
|
71
71
|
courseId: string;
|
|
72
72
|
courseName?: string;
|
|
73
73
|
|
|
74
|
+
/** User's global ELO at the time of this pipeline run */
|
|
75
|
+
userElo?: number;
|
|
76
|
+
|
|
74
77
|
// Generator phase
|
|
75
78
|
generatorName: string;
|
|
76
79
|
generators?: GeneratorSummary[];
|
|
@@ -90,6 +93,8 @@ export interface PipelineRunReport {
|
|
|
90
93
|
courseId: string;
|
|
91
94
|
origin: 'new' | 'review' | 'unknown';
|
|
92
95
|
finalScore: number;
|
|
96
|
+
/** Card's ELO (parsed from ELO generator provenance, if available) */
|
|
97
|
+
cardElo?: number;
|
|
93
98
|
provenance: StrategyContribution[];
|
|
94
99
|
tags?: string[];
|
|
95
100
|
selected: boolean;
|
|
@@ -133,6 +138,17 @@ export function captureRun(report: Omit<PipelineRunReport, 'runId' | 'timestamp'
|
|
|
133
138
|
/**
|
|
134
139
|
* Build a capture-ready report from pipeline execution data.
|
|
135
140
|
*/
|
|
141
|
+
/**
|
|
142
|
+
* Parse card ELO from the ELO generator's provenance reason string.
|
|
143
|
+
* Format: "ELO distance XX (card: YYYY, user: ZZZZ), ..."
|
|
144
|
+
*/
|
|
145
|
+
function parseCardElo(provenance: StrategyContribution[]): number | undefined {
|
|
146
|
+
const eloEntry = provenance.find((p) => p.strategy === 'elo');
|
|
147
|
+
if (!eloEntry?.reason) return undefined;
|
|
148
|
+
const match = eloEntry.reason.match(/card:\s*(\d+)/);
|
|
149
|
+
return match ? parseInt(match[1], 10) : undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
136
152
|
export function buildRunReport(
|
|
137
153
|
courseId: string,
|
|
138
154
|
courseName: string | undefined,
|
|
@@ -141,7 +157,8 @@ export function buildRunReport(
|
|
|
141
157
|
generatedCount: number,
|
|
142
158
|
filters: FilterImpact[],
|
|
143
159
|
allCards: WeightedCard[],
|
|
144
|
-
selectedCards: WeightedCard[]
|
|
160
|
+
selectedCards: WeightedCard[],
|
|
161
|
+
userElo?: number
|
|
145
162
|
): Omit<PipelineRunReport, 'runId' | 'timestamp'> {
|
|
146
163
|
const selectedIds = new Set(selectedCards.map((c) => c.cardId));
|
|
147
164
|
|
|
@@ -150,6 +167,7 @@ export function buildRunReport(
|
|
|
150
167
|
courseId: card.courseId,
|
|
151
168
|
origin: getOrigin(card),
|
|
152
169
|
finalScore: card.score,
|
|
170
|
+
cardElo: parseCardElo(card.provenance),
|
|
153
171
|
provenance: card.provenance,
|
|
154
172
|
tags: card.tags,
|
|
155
173
|
selected: selectedIds.has(card.cardId),
|
|
@@ -161,6 +179,7 @@ export function buildRunReport(
|
|
|
161
179
|
return {
|
|
162
180
|
courseId,
|
|
163
181
|
courseName,
|
|
182
|
+
userElo,
|
|
164
183
|
generatorName,
|
|
165
184
|
generators,
|
|
166
185
|
generatedCount,
|
|
@@ -203,6 +222,7 @@ function printRunSummary(run: PipelineRunReport): void {
|
|
|
203
222
|
console.group(`🔍 Pipeline Run: ${run.courseId} (${run.courseName || 'unnamed'})`);
|
|
204
223
|
logger.info(`Run ID: ${run.runId}`);
|
|
205
224
|
logger.info(`Time: ${run.timestamp.toISOString()}`);
|
|
225
|
+
logger.info(`User ELO: ${run.userElo ?? 'unknown'}`);
|
|
206
226
|
logger.info(`Generator: ${run.generatorName} → ${run.generatedCount} candidates`);
|
|
207
227
|
|
|
208
228
|
if (run.generators && run.generators.length > 0) {
|
|
@@ -293,8 +313,12 @@ export const pipelineDebugAPI = {
|
|
|
293
313
|
console.group(`🎴 Card: ${cardId}`);
|
|
294
314
|
logger.info(`Course: ${card.courseId}`);
|
|
295
315
|
logger.info(`Origin: ${card.origin}`);
|
|
316
|
+
logger.info(`Card ELO: ${card.cardElo ?? 'unknown'} | User ELO: ${run.userElo ?? 'unknown'}`);
|
|
296
317
|
logger.info(`Final score: ${card.finalScore.toFixed(3)}`);
|
|
297
318
|
logger.info(`Selected: ${card.selected ? 'Yes ✅' : 'No ❌'}`);
|
|
319
|
+
if (card.tags && card.tags.length > 0) {
|
|
320
|
+
logger.info(`Tags (${card.tags.length}): ${card.tags.join(', ')}`);
|
|
321
|
+
}
|
|
298
322
|
logger.info('Provenance:');
|
|
299
323
|
logger.info(formatProvenance(card.provenance));
|
|
300
324
|
// eslint-disable-next-line no-console
|
|
@@ -492,6 +516,29 @@ export const pipelineDebugAPI = {
|
|
|
492
516
|
return _activePipeline.diagnoseCardSpace({ threshold });
|
|
493
517
|
},
|
|
494
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Show user's per-tag ELO data. Useful for diagnosing hierarchy gate status.
|
|
521
|
+
*
|
|
522
|
+
* @param tagFilter - Optional glob pattern(s) to filter tags.
|
|
523
|
+
* Examples: 'gpc:expose:*', 'gpc:intro:t-T', ['gpc:expose:t-*', 'gpc:intro:t-*']
|
|
524
|
+
*/
|
|
525
|
+
async showTagElo(tagFilter?: string | string[]): Promise<void> {
|
|
526
|
+
if (!_activePipeline) {
|
|
527
|
+
logger.info('[Pipeline Debug] No active pipeline. Run a session first.');
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const status = await _activePipeline.getTagEloStatus(tagFilter);
|
|
531
|
+
const entries = Object.entries(status).sort(([a], [b]) => a.localeCompare(b));
|
|
532
|
+
if (entries.length === 0) {
|
|
533
|
+
logger.info(`[Pipeline Debug] No tag ELO data found${tagFilter ? ` for pattern: ${tagFilter}` : ''}.`);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
// eslint-disable-next-line no-console
|
|
537
|
+
console.table(
|
|
538
|
+
Object.fromEntries(entries.map(([tag, data]) => [tag, { score: Math.round(data.score), count: data.count }]))
|
|
539
|
+
);
|
|
540
|
+
},
|
|
541
|
+
|
|
495
542
|
/**
|
|
496
543
|
* Show help.
|
|
497
544
|
*/
|
|
@@ -503,6 +550,7 @@ Commands:
|
|
|
503
550
|
.showLastRun() Show summary of most recent pipeline run
|
|
504
551
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
505
552
|
.showCard(cardId) Show provenance trail for a specific card
|
|
553
|
+
.showTagElo(pattern) Show user's tag ELO data (async). E.g. 'gpc:expose:*'
|
|
506
554
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
507
555
|
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
508
556
|
.showRegistry() Show navigator registry (classes + roles)
|
|
@@ -5,6 +5,7 @@ import type { WeightedCard } from '../index';
|
|
|
5
5
|
import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
|
|
6
6
|
import type { CardFilter, FilterContext } from './types';
|
|
7
7
|
import { toCourseElo } from '@vue-skuilder/common';
|
|
8
|
+
import { logger } from '../../../util/logger';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* A single prerequisite requirement for a tag.
|
|
@@ -29,6 +30,18 @@ interface TagPrerequisite {
|
|
|
29
30
|
* tagged `gpc:expose:t-T` while `gpc:intro:t-T` is still locked.
|
|
30
31
|
*/
|
|
31
32
|
preReqBoost?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Score multiplier applied to cards carrying the *gated* tag once the
|
|
35
|
+
* gate opens (all prereqs met). Ensures newly-unlocked content surfaces
|
|
36
|
+
* promptly before natural ELO/SRS scoring takes over.
|
|
37
|
+
*
|
|
38
|
+
* Falls away naturally as generators stop surfacing the card after
|
|
39
|
+
* the user interacts with it.
|
|
40
|
+
*
|
|
41
|
+
* Example: `targetBoost: 4` on intro-t-T's prerequisite gives a 4×
|
|
42
|
+
* score increase to intro-t-T cards once their expose gate is met.
|
|
43
|
+
*/
|
|
44
|
+
targetBoost?: number;
|
|
32
45
|
}
|
|
33
46
|
|
|
34
47
|
/**
|
|
@@ -239,21 +252,68 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
239
252
|
return boosts;
|
|
240
253
|
}
|
|
241
254
|
|
|
255
|
+
/**
|
|
256
|
+
* Build a map of gated tag → max configured targetBoost for all *open* gates.
|
|
257
|
+
*
|
|
258
|
+
* When a gate opens (prereqs met), cards carrying the gated tag get boosted —
|
|
259
|
+
* ensuring newly-unlocked content surfaces promptly. The boost is a static
|
|
260
|
+
* multiplier; natural ELO/SRS deprioritization after interaction handles decay.
|
|
261
|
+
*/
|
|
262
|
+
private getTargetBoosts(unlockedTags: Set<string>): Map<string, number> {
|
|
263
|
+
const boosts = new Map<string, number>();
|
|
264
|
+
|
|
265
|
+
const configKeys = Object.keys(this.config.prerequisites);
|
|
266
|
+
const unlockedArr = [...unlockedTags];
|
|
267
|
+
logger.info(
|
|
268
|
+
`[HierarchyDefinition:targetBoost:trace] ${this.name} | configKeys=${configKeys.length}, unlocked=${unlockedArr.length} (${unlockedArr.slice(0, 5).join(', ')}${unlockedArr.length > 5 ? '...' : ''})`
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
|
|
272
|
+
// Only boost targets of open gates
|
|
273
|
+
if (!unlockedTags.has(tagId)) continue;
|
|
274
|
+
|
|
275
|
+
// TRACE: dump prereq details for unlocked tags
|
|
276
|
+
logger.info(
|
|
277
|
+
`[HierarchyDefinition:targetBoost:trace] UNLOCKED ${tagId}: ${prereqs.length} prereqs, raw=${JSON.stringify(prereqs.map(p => ({ tag: p.tag, tb: p.targetBoost })))}`
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
for (const prereq of prereqs) {
|
|
281
|
+
if (!prereq.targetBoost || prereq.targetBoost <= 1.0) continue;
|
|
282
|
+
|
|
283
|
+
const existing = boosts.get(tagId) ?? 1.0;
|
|
284
|
+
boosts.set(tagId, Math.max(existing, prereq.targetBoost));
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (boosts.size > 0) {
|
|
289
|
+
logger.info(
|
|
290
|
+
`[HierarchyDefinition] targetBoosts active: ${[...boosts.entries()].map(([t, b]) => `${t}=×${b}`).join(', ')}`
|
|
291
|
+
);
|
|
292
|
+
} else {
|
|
293
|
+
logger.info(
|
|
294
|
+
`[HierarchyDefinition:targetBoost:trace] no targetBoosts found despite ${unlockedArr.length} unlocked tags`
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return boosts;
|
|
299
|
+
}
|
|
300
|
+
|
|
242
301
|
/**
|
|
243
302
|
* CardFilter.transform implementation.
|
|
244
303
|
*
|
|
245
|
-
*
|
|
246
|
-
* 1. Cards with locked tags receive score * 0.
|
|
247
|
-
* 2. Cards carrying prereq tags of closed gates receive
|
|
248
|
-
*
|
|
304
|
+
* Three effects:
|
|
305
|
+
* 1. Cards with locked tags receive score * 0.02 (gating penalty)
|
|
306
|
+
* 2. Cards carrying prereq tags of closed gates receive preReqBoost
|
|
307
|
+
* 3. Cards carrying gated tags of open gates receive targetBoost
|
|
249
308
|
*/
|
|
250
309
|
async transform(cards: WeightedCard[], context: FilterContext): Promise<WeightedCard[]> {
|
|
251
310
|
// Get mastery state
|
|
252
311
|
const masteredTags = await this.getMasteredTags(context);
|
|
253
312
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
254
313
|
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
314
|
+
const targetBoosts = this.getTargetBoosts(unlockedTags);
|
|
255
315
|
|
|
256
|
-
// Apply prerequisite gating + prereq boosting
|
|
316
|
+
// Apply prerequisite gating + prereq/target boosting
|
|
257
317
|
const gated: WeightedCard[] = [];
|
|
258
318
|
|
|
259
319
|
for (const card of cards) {
|
|
@@ -286,6 +346,33 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
286
346
|
finalScore *= maxBoost;
|
|
287
347
|
action = 'boosted';
|
|
288
348
|
finalReason = `${reason} | preReqBoost ×${maxBoost.toFixed(2)} for ${boostedPrereqs.join(', ')}`;
|
|
349
|
+
logger.info(
|
|
350
|
+
`[HierarchyDefinition] preReqBoost ×${maxBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedPrereqs.join(', ')}] (score: ${card.score.toFixed(3)} → ${finalScore.toFixed(3)})`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Apply target boost to unlocked cards whose gated tag just opened
|
|
356
|
+
if (isUnlocked && targetBoosts.size > 0) {
|
|
357
|
+
const cardTags = card.tags ?? [];
|
|
358
|
+
let maxTargetBoost = 1.0;
|
|
359
|
+
const boostedTargets: string[] = [];
|
|
360
|
+
|
|
361
|
+
for (const tag of cardTags) {
|
|
362
|
+
const boost = targetBoosts.get(tag);
|
|
363
|
+
if (boost && boost > maxTargetBoost) {
|
|
364
|
+
maxTargetBoost = boost;
|
|
365
|
+
boostedTargets.push(tag);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (maxTargetBoost > 1.0) {
|
|
370
|
+
finalScore *= maxTargetBoost;
|
|
371
|
+
action = 'boosted';
|
|
372
|
+
finalReason = `${finalReason} | targetBoost ×${maxTargetBoost.toFixed(2)} for ${boostedTargets.join(', ')}`;
|
|
373
|
+
logger.info(
|
|
374
|
+
`[HierarchyDefinition] targetBoost ×${maxTargetBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedTargets.join(', ')}] (score: ${card.score.toFixed(3)} → ${finalScore.toFixed(3)})`
|
|
375
|
+
);
|
|
289
376
|
}
|
|
290
377
|
}
|
|
291
378
|
|
|
@@ -198,7 +198,13 @@ export default class RelativePriorityNavigator extends ContentNavigator implemen
|
|
|
198
198
|
const cardTags = card.tags ?? [];
|
|
199
199
|
const priority = this.computeCardPriority(cardTags);
|
|
200
200
|
const boostFactor = this.computeBoostFactor(priority);
|
|
201
|
-
|
|
201
|
+
// No upper clamp — scores may exceed 1.0 intentionally.
|
|
202
|
+
// Scores are only used for relative ordering within a pipeline run,
|
|
203
|
+
// so absolute magnitude doesn't matter. Clamping to 1.0 here collapsed
|
|
204
|
+
// differentiation when GPC preReqBoosts and priority boosts compounded
|
|
205
|
+
// (e.g. 0.96 × 2.5 × 1.24 → 2.98, previously crushed back to 1.0).
|
|
206
|
+
// Floor of 0 is kept: negative scores have no meaning.
|
|
207
|
+
const finalScore = Math.max(0, card.score * boostFactor);
|
|
202
208
|
|
|
203
209
|
// Determine action based on boost factor
|
|
204
210
|
const action = boostFactor > 1.0 ? 'boosted' : boostFactor < 1.0 ? 'penalized' : 'passed';
|