@vue-skuilder/db 0.2.5 → 0.2.8
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 +75 -1
- package/dist/core/index.d.ts +75 -1
- package/dist/core/index.js +281 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +277 -4
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +273 -4
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +273 -4
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +273 -4
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +273 -4
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +307 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +303 -7
- package/dist/index.mjs.map +1 -1
- package/docs/navigators-architecture.md +42 -2
- package/package.json +3 -3
- package/src/core/navigators/Pipeline.ts +103 -2
- package/src/core/navigators/PipelineDebugger.ts +11 -1
- package/src/core/navigators/diversityRerank.ts +185 -0
- package/src/core/navigators/generators/prescribed.ts +173 -1
- package/src/core/navigators/index.ts +12 -0
- package/src/study/ItemQueue.test.ts +71 -0
- package/src/study/ItemQueue.ts +19 -1
- package/src/study/SessionController.ts +20 -5
|
@@ -522,12 +522,102 @@ var init_courseLookupDB = __esm({
|
|
|
522
522
|
}
|
|
523
523
|
});
|
|
524
524
|
|
|
525
|
+
// src/core/navigators/diversityRerank.ts
|
|
526
|
+
var diversityRerank_exports = {};
|
|
527
|
+
__export(diversityRerank_exports, {
|
|
528
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
529
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
530
|
+
diversityRerank: () => diversityRerank
|
|
531
|
+
});
|
|
532
|
+
function diversityRerank(cards, opts = {}) {
|
|
533
|
+
const strength = opts.strength ?? DIVERSITY_STRENGTH;
|
|
534
|
+
const floor = opts.floor ?? DIVERSITY_FLOOR;
|
|
535
|
+
const n = cards.length;
|
|
536
|
+
if (n <= 1) return cards;
|
|
537
|
+
const df = /* @__PURE__ */ new Map();
|
|
538
|
+
for (const card of cards) {
|
|
539
|
+
for (const tag of card.tags ?? []) {
|
|
540
|
+
df.set(tag, (df.get(tag) ?? 0) + 1);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
const idf = /* @__PURE__ */ new Map();
|
|
544
|
+
for (const [tag, freq] of df) {
|
|
545
|
+
idf.set(tag, Math.log(n / freq));
|
|
546
|
+
}
|
|
547
|
+
const remaining = [...cards];
|
|
548
|
+
const emittedCount = /* @__PURE__ */ new Map();
|
|
549
|
+
const out = [];
|
|
550
|
+
const repetitionLoad = (card) => {
|
|
551
|
+
let load = 0;
|
|
552
|
+
for (const tag of card.tags ?? []) {
|
|
553
|
+
const seen = emittedCount.get(tag);
|
|
554
|
+
if (seen) load += (idf.get(tag) ?? 0) * seen;
|
|
555
|
+
}
|
|
556
|
+
return load;
|
|
557
|
+
};
|
|
558
|
+
while (remaining.length > 0) {
|
|
559
|
+
let bestIdx = 0;
|
|
560
|
+
let bestValue = -Infinity;
|
|
561
|
+
let bestPenalty = 1;
|
|
562
|
+
let bestLoad = 0;
|
|
563
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
564
|
+
const card = remaining[i];
|
|
565
|
+
const load = repetitionLoad(card);
|
|
566
|
+
const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
|
|
567
|
+
const value = card.score * penalty;
|
|
568
|
+
if (value > bestValue) {
|
|
569
|
+
bestValue = value;
|
|
570
|
+
bestIdx = i;
|
|
571
|
+
bestPenalty = penalty;
|
|
572
|
+
bestLoad = load;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const [picked] = remaining.splice(bestIdx, 1);
|
|
576
|
+
if (Number.isFinite(picked.score) && bestPenalty < 1) {
|
|
577
|
+
const newScore = picked.score * bestPenalty;
|
|
578
|
+
out.push({
|
|
579
|
+
...picked,
|
|
580
|
+
score: newScore,
|
|
581
|
+
provenance: [
|
|
582
|
+
...picked.provenance,
|
|
583
|
+
{
|
|
584
|
+
strategy: STRATEGY,
|
|
585
|
+
strategyId: STRATEGY_ID,
|
|
586
|
+
strategyName: STRATEGY_NAME,
|
|
587
|
+
action: "penalized",
|
|
588
|
+
score: newScore,
|
|
589
|
+
reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
|
|
590
|
+
}
|
|
591
|
+
]
|
|
592
|
+
});
|
|
593
|
+
} else {
|
|
594
|
+
out.push(picked);
|
|
595
|
+
}
|
|
596
|
+
for (const tag of picked.tags ?? []) {
|
|
597
|
+
emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return out;
|
|
601
|
+
}
|
|
602
|
+
var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
|
|
603
|
+
var init_diversityRerank = __esm({
|
|
604
|
+
"src/core/navigators/diversityRerank.ts"() {
|
|
605
|
+
"use strict";
|
|
606
|
+
DIVERSITY_STRENGTH = 0.6;
|
|
607
|
+
DIVERSITY_FLOOR = 0.3;
|
|
608
|
+
STRATEGY = "diversityRerank";
|
|
609
|
+
STRATEGY_ID = "DIVERSITY_RERANK";
|
|
610
|
+
STRATEGY_NAME = "Diversity Re-rank";
|
|
611
|
+
}
|
|
612
|
+
});
|
|
613
|
+
|
|
525
614
|
// src/core/navigators/PipelineDebugger.ts
|
|
526
615
|
var PipelineDebugger_exports = {};
|
|
527
616
|
__export(PipelineDebugger_exports, {
|
|
528
617
|
buildRunReport: () => buildRunReport,
|
|
529
618
|
captureRun: () => captureRun,
|
|
530
619
|
clearRunHistory: () => clearRunHistory,
|
|
620
|
+
getActivePipeline: () => getActivePipeline,
|
|
531
621
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
532
622
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
533
623
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -535,6 +625,9 @@ __export(PipelineDebugger_exports, {
|
|
|
535
625
|
function registerPipelineForDebug(pipeline) {
|
|
536
626
|
_activePipeline = pipeline;
|
|
537
627
|
}
|
|
628
|
+
function getActivePipeline() {
|
|
629
|
+
return _activePipeline;
|
|
630
|
+
}
|
|
538
631
|
function clearRunHistory() {
|
|
539
632
|
runHistory.length = 0;
|
|
540
633
|
}
|
|
@@ -1717,7 +1810,7 @@ function shuffleInPlace(arr) {
|
|
|
1717
1810
|
function pickTopByScore(cards, limit) {
|
|
1718
1811
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1719
1812
|
}
|
|
1720
|
-
var DEFAULT_FRESHNESS_WINDOW, DEFAULT_MAX_DIRECT_PER_RUN, DEFAULT_MAX_SUPPORT_PER_RUN, DEFAULT_HIERARCHY_DEPTH, DEFAULT_MIN_COUNT, BASE_TARGET_SCORE, BASE_SUPPORT_SCORE, DISCOVERED_SUPPORT_SCORE, MAX_TARGET_MULTIPLIER, MAX_SUPPORT_MULTIPLIER, PRESCRIBED_DEBUG_VERSION, PrescribedCardsGenerator;
|
|
1813
|
+
var DEFAULT_FRESHNESS_WINDOW, DEFAULT_MAX_DIRECT_PER_RUN, DEFAULT_MAX_SUPPORT_PER_RUN, DEFAULT_HIERARCHY_DEPTH, DEFAULT_MIN_COUNT, DEFAULT_PRACTICE_MIN_COUNT, DEFAULT_MAX_PRACTICE_PER_RUN, BASE_TARGET_SCORE, BASE_SUPPORT_SCORE, DISCOVERED_SUPPORT_SCORE, BASE_PRACTICE_SCORE, MAX_TARGET_MULTIPLIER, MAX_SUPPORT_MULTIPLIER, PRESCRIBED_DEBUG_VERSION, PrescribedCardsGenerator;
|
|
1721
1814
|
var init_prescribed = __esm({
|
|
1722
1815
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1723
1816
|
"use strict";
|
|
@@ -1728,9 +1821,12 @@ var init_prescribed = __esm({
|
|
|
1728
1821
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1729
1822
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1730
1823
|
DEFAULT_MIN_COUNT = 3;
|
|
1824
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
1825
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
1731
1826
|
BASE_TARGET_SCORE = 1;
|
|
1732
1827
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1733
1828
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
1829
|
+
BASE_PRACTICE_SCORE = 1;
|
|
1734
1830
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1735
1831
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1736
1832
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -1838,7 +1934,18 @@ var init_prescribed = __esm({
|
|
|
1838
1934
|
courseId,
|
|
1839
1935
|
emittedIds
|
|
1840
1936
|
);
|
|
1841
|
-
|
|
1937
|
+
const practiceCards = this.buildPracticeCards({
|
|
1938
|
+
group,
|
|
1939
|
+
courseId,
|
|
1940
|
+
emittedIds,
|
|
1941
|
+
cardsByTag,
|
|
1942
|
+
hierarchyConfigs,
|
|
1943
|
+
userTagElo,
|
|
1944
|
+
userGlobalElo,
|
|
1945
|
+
activeIds,
|
|
1946
|
+
seenIds
|
|
1947
|
+
});
|
|
1948
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
1842
1949
|
}
|
|
1843
1950
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
1844
1951
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -1866,6 +1973,7 @@ var init_prescribed = __esm({
|
|
|
1866
1973
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
1867
1974
|
for (const card of finalCards) {
|
|
1868
1975
|
const prov = card.provenance[0];
|
|
1976
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
1869
1977
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
1870
1978
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
1871
1979
|
if (!groupId) continue;
|
|
@@ -1935,7 +2043,12 @@ var init_prescribed = __esm({
|
|
|
1935
2043
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
1936
2044
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
1937
2045
|
},
|
|
1938
|
-
retireOnEncounter: raw.retireOnEncounter !== false
|
|
2046
|
+
retireOnEncounter: raw.retireOnEncounter !== false,
|
|
2047
|
+
practiceTagPatterns: dedupe(
|
|
2048
|
+
Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
|
|
2049
|
+
),
|
|
2050
|
+
practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
|
|
2051
|
+
maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
|
|
1939
2052
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
1940
2053
|
return { groups };
|
|
1941
2054
|
} catch {
|
|
@@ -2158,6 +2271,92 @@ var init_prescribed = __esm({
|
|
|
2158
2271
|
}
|
|
2159
2272
|
return cards;
|
|
2160
2273
|
}
|
|
2274
|
+
/**
|
|
2275
|
+
* Emit drill cards for *unlocked-but-under-practiced* skills.
|
|
2276
|
+
*
|
|
2277
|
+
* For each course tag matching the group's `practiceTagPatterns` that is both
|
|
2278
|
+
* unlocked (all hierarchy prerequisites met — i.e. the learner has been
|
|
2279
|
+
* introduced to it) and under-practiced (per-tag attempt count below
|
|
2280
|
+
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2281
|
+
* into the candidate pool. It exists because global-ELO retrieval
|
|
2282
|
+
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2283
|
+
* freshly-introduced skill — putting them in the pool here lets the pipeline's
|
|
2284
|
+
* scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
|
|
2285
|
+
* this method's job; it only guarantees presence.
|
|
2286
|
+
*
|
|
2287
|
+
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2288
|
+
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
2289
|
+
*/
|
|
2290
|
+
buildPracticeCards(args) {
|
|
2291
|
+
const {
|
|
2292
|
+
group,
|
|
2293
|
+
courseId,
|
|
2294
|
+
emittedIds,
|
|
2295
|
+
cardsByTag,
|
|
2296
|
+
hierarchyConfigs,
|
|
2297
|
+
userTagElo,
|
|
2298
|
+
userGlobalElo,
|
|
2299
|
+
activeIds,
|
|
2300
|
+
seenIds
|
|
2301
|
+
} = args;
|
|
2302
|
+
const patterns = group.practiceTagPatterns ?? [];
|
|
2303
|
+
if (patterns.length === 0) return [];
|
|
2304
|
+
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2305
|
+
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2306
|
+
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2307
|
+
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2308
|
+
);
|
|
2309
|
+
if (practiceTags.length === 0) return [];
|
|
2310
|
+
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2311
|
+
supportTags: practiceTags,
|
|
2312
|
+
cardsByTag,
|
|
2313
|
+
activeIds,
|
|
2314
|
+
seenIds,
|
|
2315
|
+
excludedIds: emittedIds,
|
|
2316
|
+
limit: maxPractice
|
|
2317
|
+
});
|
|
2318
|
+
if (practiceCardIds.length === 0) return [];
|
|
2319
|
+
logger.info(
|
|
2320
|
+
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2321
|
+
);
|
|
2322
|
+
const cards = [];
|
|
2323
|
+
for (const cardId of practiceCardIds) {
|
|
2324
|
+
emittedIds.add(cardId);
|
|
2325
|
+
cards.push({
|
|
2326
|
+
cardId,
|
|
2327
|
+
courseId,
|
|
2328
|
+
score: BASE_PRACTICE_SCORE,
|
|
2329
|
+
provenance: [
|
|
2330
|
+
{
|
|
2331
|
+
strategy: "prescribed",
|
|
2332
|
+
strategyName: this.strategyName || this.name,
|
|
2333
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2334
|
+
action: "generated",
|
|
2335
|
+
score: BASE_PRACTICE_SCORE,
|
|
2336
|
+
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2337
|
+
}
|
|
2338
|
+
]
|
|
2339
|
+
});
|
|
2340
|
+
}
|
|
2341
|
+
return cards;
|
|
2342
|
+
}
|
|
2343
|
+
/**
|
|
2344
|
+
* True for a skill that was *gated and is now reached*: it has at least one
|
|
2345
|
+
* declared hierarchy prerequisite set, and every set is fully satisfied by the
|
|
2346
|
+
* learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
|
|
2347
|
+
* — an ungated tag was never "introduced" in the curricular sense, so it isn't
|
|
2348
|
+
* a post-intro drill target (e.g. whole-word spelling tags that share the
|
|
2349
|
+
* `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
|
|
2350
|
+
* ELO retrieval. This is the precise population the retrieval gap strands:
|
|
2351
|
+
* just-unlocked, low-ELO skills.
|
|
2352
|
+
*/
|
|
2353
|
+
isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
|
|
2354
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
2355
|
+
if (prereqSets.length === 0) return false;
|
|
2356
|
+
return prereqSets.every(
|
|
2357
|
+
(prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
2358
|
+
);
|
|
2359
|
+
}
|
|
2161
2360
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2162
2361
|
if (supportTags.length === 0) {
|
|
2163
2362
|
return [];
|
|
@@ -3705,7 +3904,7 @@ function logResultCards(cards) {
|
|
|
3705
3904
|
for (let i = 0; i < cards.length; i++) {
|
|
3706
3905
|
const c = cards[i];
|
|
3707
3906
|
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
3708
|
-
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
3907
|
+
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint" || p.strategy === "diversityRerank").map((p) => {
|
|
3709
3908
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
3710
3909
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
3711
3910
|
}).join(" | ");
|
|
@@ -3737,6 +3936,7 @@ var init_Pipeline = __esm({
|
|
|
3737
3936
|
init_logger();
|
|
3738
3937
|
init_orchestration();
|
|
3739
3938
|
init_PipelineDebugger();
|
|
3939
|
+
init_diversityRerank();
|
|
3740
3940
|
VERBOSE_RESULTS = true;
|
|
3741
3941
|
Pipeline = class extends ContentNavigator {
|
|
3742
3942
|
generator;
|
|
@@ -3910,6 +4110,7 @@ var init_Pipeline = __esm({
|
|
|
3910
4110
|
this._ephemeralHints = null;
|
|
3911
4111
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
3912
4112
|
}
|
|
4113
|
+
cards = diversityRerank(cards);
|
|
3913
4114
|
cards.sort((a, b) => b.score - a.score);
|
|
3914
4115
|
const tFilter = performance.now();
|
|
3915
4116
|
const result = cards.slice(0, limit);
|
|
@@ -4213,6 +4414,68 @@ var init_Pipeline = __esm({
|
|
|
4213
4414
|
// ---------------------------------------------------------------------------
|
|
4214
4415
|
// Card-space diagnostic
|
|
4215
4416
|
// ---------------------------------------------------------------------------
|
|
4417
|
+
/**
|
|
4418
|
+
* Commit-free forecast: score the user's full card space through the filter
|
|
4419
|
+
* chain and return the cards that are currently *reachable* (score >=
|
|
4420
|
+
* threshold), optionally nudged by caller-supplied hints and/or restricted
|
|
4421
|
+
* to cards the user hasn't seen yet.
|
|
4422
|
+
*
|
|
4423
|
+
* This is a GENERIC primitive — it returns scored, tag-hydrated cards and
|
|
4424
|
+
* stops there. It has no knowledge of any particular tag convention; callers
|
|
4425
|
+
* decide what the surviving cards mean (e.g. filter to their own "intro"
|
|
4426
|
+
* tag family). Nothing is written and no session is started.
|
|
4427
|
+
*
|
|
4428
|
+
* The optional `hints` are the "out-of-band kick": they run through the same
|
|
4429
|
+
* {@link applyHints} path a live replan uses, so the two semantics carry over —
|
|
4430
|
+
* - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
|
|
4431
|
+
* stays out), and
|
|
4432
|
+
* - `requireTags`/`requireCards` inject from the full pre-filter pool,
|
|
4433
|
+
* *bypassing* gating (use when you want a card regardless of reachability).
|
|
4434
|
+
* Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
|
|
4435
|
+
* user has already seen — pass `unseenOnly: false` if that matters.
|
|
4436
|
+
*
|
|
4437
|
+
* Cost note: like {@link diagnoseCardSpace}, this scans every card through the
|
|
4438
|
+
* filters, so it's heavier than a normal replan. Intended for one-shot
|
|
4439
|
+
* out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
|
|
4440
|
+
*
|
|
4441
|
+
* @param opts.hints Optional ephemeral hints to apply after the filter chain.
|
|
4442
|
+
* @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
|
|
4443
|
+
* @param opts.threshold Min score to count as reachable (default 0.10).
|
|
4444
|
+
* @param opts.limit Optional cap on results (already sorted desc).
|
|
4445
|
+
*/
|
|
4446
|
+
async forecast(opts) {
|
|
4447
|
+
const threshold = opts?.threshold ?? 0.1;
|
|
4448
|
+
const unseenOnly = opts?.unseenOnly ?? true;
|
|
4449
|
+
const courseId = this.course.getCourseID();
|
|
4450
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
4451
|
+
let cards = allCardIds.map((cardId) => ({
|
|
4452
|
+
cardId,
|
|
4453
|
+
courseId,
|
|
4454
|
+
score: 1,
|
|
4455
|
+
provenance: []
|
|
4456
|
+
}));
|
|
4457
|
+
cards = await this.hydrateTags(cards);
|
|
4458
|
+
const fullPool = cards.slice();
|
|
4459
|
+
const context = await this.buildContext();
|
|
4460
|
+
for (const filter of this.filters) {
|
|
4461
|
+
cards = await filter.transform(cards, context);
|
|
4462
|
+
}
|
|
4463
|
+
if (opts?.hints) {
|
|
4464
|
+
cards = this.applyHints(cards, opts.hints, fullPool);
|
|
4465
|
+
}
|
|
4466
|
+
cards = cards.filter((c) => c.score >= threshold);
|
|
4467
|
+
if (unseenOnly) {
|
|
4468
|
+
let encountered;
|
|
4469
|
+
try {
|
|
4470
|
+
encountered = new Set(await this.user.getSeenCards(courseId));
|
|
4471
|
+
} catch {
|
|
4472
|
+
encountered = /* @__PURE__ */ new Set();
|
|
4473
|
+
}
|
|
4474
|
+
cards = cards.filter((c) => !encountered.has(c.cardId));
|
|
4475
|
+
}
|
|
4476
|
+
cards.sort((a, b) => b.score - a.score);
|
|
4477
|
+
return opts?.limit ? cards.slice(0, opts.limit) : cards;
|
|
4478
|
+
}
|
|
4216
4479
|
/**
|
|
4217
4480
|
* Scan every card in the course through the filter chain and report
|
|
4218
4481
|
* how many are "well indicated" (score >= threshold) for the current user.
|
|
@@ -4478,6 +4741,7 @@ var init_3 = __esm({
|
|
|
4478
4741
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4479
4742
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4480
4743
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
4744
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4481
4745
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
4482
4746
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
4483
4747
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -4503,9 +4767,13 @@ var init_3 = __esm({
|
|
|
4503
4767
|
var navigators_exports = {};
|
|
4504
4768
|
__export(navigators_exports, {
|
|
4505
4769
|
ContentNavigator: () => ContentNavigator,
|
|
4770
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
4771
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
4506
4772
|
NavigatorRole: () => NavigatorRole,
|
|
4507
4773
|
NavigatorRoles: () => NavigatorRoles,
|
|
4508
4774
|
Navigators: () => Navigators,
|
|
4775
|
+
diversityRerank: () => diversityRerank,
|
|
4776
|
+
getActivePipeline: () => getActivePipeline,
|
|
4509
4777
|
getCardOrigin: () => getCardOrigin,
|
|
4510
4778
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
4511
4779
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -4589,6 +4857,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
4589
4857
|
var init_navigators = __esm({
|
|
4590
4858
|
"src/core/navigators/index.ts"() {
|
|
4591
4859
|
"use strict";
|
|
4860
|
+
init_diversityRerank();
|
|
4592
4861
|
init_PipelineDebugger();
|
|
4593
4862
|
init_logger();
|
|
4594
4863
|
init_();
|