@vue-skuilder/db 0.1.32-b → 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 +2262 -223
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +2239 -196
- 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 +17 -4
- package/dist/impl/couch/index.d.ts +17 -4
- package/dist/impl/couch/index.js +2306 -220
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +2294 -204
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +4 -5
- package/dist/impl/static/index.d.ts +4 -5
- package/dist/impl/static/index.js +2266 -227
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +2251 -208
- 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 -444
- package/dist/index.d.ts +9 -444
- package/dist/index.js +9637 -8931
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9539 -8833
- 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 +47 -29
- package/src/core/navigators/PipelineDebugger.ts +49 -1
- package/src/core/navigators/filters/hierarchyDefinition.ts +88 -5
- 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 +3 -2
- package/src/impl/static/courseDB.ts +3 -2
- package/src/study/SessionController.ts +79 -9
- 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,31 +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
|
-
/**
|
|
43
|
-
* Debugging label threaded from the replan requester.
|
|
44
|
-
* Attached to provenance entries so card scoring history
|
|
45
|
-
* can be traced back to the originating event.
|
|
46
|
-
* Prefixed with `_` to signal it's metadata, not a scoring hint.
|
|
47
|
-
*/
|
|
48
|
-
_label?: string;
|
|
49
|
-
}
|
|
25
|
+
// ReplanHints is the canonical type — re-export for consumers that import from Pipeline
|
|
26
|
+
import { ReplanHints } from '@db/study/SessionController';
|
|
27
|
+
export { ReplanHints };
|
|
50
28
|
|
|
51
29
|
/**
|
|
52
30
|
* Convert a glob pattern (with `*` wildcards) to a RegExp.
|
|
@@ -296,8 +274,8 @@ export class Pipeline extends ContentNavigator {
|
|
|
296
274
|
*
|
|
297
275
|
* Overrides ContentNavigator.setEphemeralHints() no-op.
|
|
298
276
|
*/
|
|
299
|
-
override setEphemeralHints(hints:
|
|
300
|
-
this._ephemeralHints = hints
|
|
277
|
+
override setEphemeralHints(hints: ReplanHints): void {
|
|
278
|
+
this._ephemeralHints = hints;
|
|
301
279
|
logger.info(`[Pipeline] Ephemeral hints set: ${JSON.stringify(hints)}`);
|
|
302
280
|
}
|
|
303
281
|
|
|
@@ -439,6 +417,9 @@ export class Pipeline extends ContentNavigator {
|
|
|
439
417
|
// Capture run for debug API
|
|
440
418
|
try {
|
|
441
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.
|
|
442
423
|
const report = buildRunReport(
|
|
443
424
|
this.course?.getCourseID() || 'unknown',
|
|
444
425
|
courseName,
|
|
@@ -446,8 +427,9 @@ export class Pipeline extends ContentNavigator {
|
|
|
446
427
|
generatorSummaries,
|
|
447
428
|
generatedCount,
|
|
448
429
|
filterImpacts,
|
|
449
|
-
|
|
450
|
-
result
|
|
430
|
+
cards,
|
|
431
|
+
result,
|
|
432
|
+
context.userElo
|
|
451
433
|
);
|
|
452
434
|
captureRun(report);
|
|
453
435
|
} catch (e) {
|
|
@@ -707,6 +689,42 @@ export class Pipeline extends ContentNavigator {
|
|
|
707
689
|
return [...new Set(ids)];
|
|
708
690
|
}
|
|
709
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
|
+
|
|
710
728
|
// ---------------------------------------------------------------------------
|
|
711
729
|
// Card-space diagnostic
|
|
712
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)
|
|
@@ -30,6 +30,18 @@ interface TagPrerequisite {
|
|
|
30
30
|
* tagged `gpc:expose:t-T` while `gpc:intro:t-T` is still locked.
|
|
31
31
|
*/
|
|
32
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;
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
/**
|
|
@@ -240,21 +252,68 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
240
252
|
return boosts;
|
|
241
253
|
}
|
|
242
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
|
+
|
|
243
301
|
/**
|
|
244
302
|
* CardFilter.transform implementation.
|
|
245
303
|
*
|
|
246
|
-
*
|
|
247
|
-
* 1. Cards with locked tags receive score * 0.
|
|
248
|
-
* 2. Cards carrying prereq tags of closed gates receive
|
|
249
|
-
*
|
|
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
|
|
250
308
|
*/
|
|
251
309
|
async transform(cards: WeightedCard[], context: FilterContext): Promise<WeightedCard[]> {
|
|
252
310
|
// Get mastery state
|
|
253
311
|
const masteredTags = await this.getMasteredTags(context);
|
|
254
312
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
255
313
|
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
314
|
+
const targetBoosts = this.getTargetBoosts(unlockedTags);
|
|
256
315
|
|
|
257
|
-
// Apply prerequisite gating + prereq boosting
|
|
316
|
+
// Apply prerequisite gating + prereq/target boosting
|
|
258
317
|
const gated: WeightedCard[] = [];
|
|
259
318
|
|
|
260
319
|
for (const card of cards) {
|
|
@@ -293,6 +352,30 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
293
352
|
}
|
|
294
353
|
}
|
|
295
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
|
+
);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
296
379
|
gated.push({
|
|
297
380
|
...card,
|
|
298
381
|
score: finalScore,
|