@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
|
@@ -498,12 +498,102 @@ var init_courseLookupDB = __esm({
|
|
|
498
498
|
}
|
|
499
499
|
});
|
|
500
500
|
|
|
501
|
+
// src/core/navigators/diversityRerank.ts
|
|
502
|
+
var diversityRerank_exports = {};
|
|
503
|
+
__export(diversityRerank_exports, {
|
|
504
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
505
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
506
|
+
diversityRerank: () => diversityRerank
|
|
507
|
+
});
|
|
508
|
+
function diversityRerank(cards, opts = {}) {
|
|
509
|
+
const strength = opts.strength ?? DIVERSITY_STRENGTH;
|
|
510
|
+
const floor = opts.floor ?? DIVERSITY_FLOOR;
|
|
511
|
+
const n = cards.length;
|
|
512
|
+
if (n <= 1) return cards;
|
|
513
|
+
const df = /* @__PURE__ */ new Map();
|
|
514
|
+
for (const card of cards) {
|
|
515
|
+
for (const tag of card.tags ?? []) {
|
|
516
|
+
df.set(tag, (df.get(tag) ?? 0) + 1);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const idf = /* @__PURE__ */ new Map();
|
|
520
|
+
for (const [tag, freq] of df) {
|
|
521
|
+
idf.set(tag, Math.log(n / freq));
|
|
522
|
+
}
|
|
523
|
+
const remaining = [...cards];
|
|
524
|
+
const emittedCount = /* @__PURE__ */ new Map();
|
|
525
|
+
const out = [];
|
|
526
|
+
const repetitionLoad = (card) => {
|
|
527
|
+
let load = 0;
|
|
528
|
+
for (const tag of card.tags ?? []) {
|
|
529
|
+
const seen = emittedCount.get(tag);
|
|
530
|
+
if (seen) load += (idf.get(tag) ?? 0) * seen;
|
|
531
|
+
}
|
|
532
|
+
return load;
|
|
533
|
+
};
|
|
534
|
+
while (remaining.length > 0) {
|
|
535
|
+
let bestIdx = 0;
|
|
536
|
+
let bestValue = -Infinity;
|
|
537
|
+
let bestPenalty = 1;
|
|
538
|
+
let bestLoad = 0;
|
|
539
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
540
|
+
const card = remaining[i];
|
|
541
|
+
const load = repetitionLoad(card);
|
|
542
|
+
const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
|
|
543
|
+
const value = card.score * penalty;
|
|
544
|
+
if (value > bestValue) {
|
|
545
|
+
bestValue = value;
|
|
546
|
+
bestIdx = i;
|
|
547
|
+
bestPenalty = penalty;
|
|
548
|
+
bestLoad = load;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const [picked] = remaining.splice(bestIdx, 1);
|
|
552
|
+
if (Number.isFinite(picked.score) && bestPenalty < 1) {
|
|
553
|
+
const newScore = picked.score * bestPenalty;
|
|
554
|
+
out.push({
|
|
555
|
+
...picked,
|
|
556
|
+
score: newScore,
|
|
557
|
+
provenance: [
|
|
558
|
+
...picked.provenance,
|
|
559
|
+
{
|
|
560
|
+
strategy: STRATEGY,
|
|
561
|
+
strategyId: STRATEGY_ID,
|
|
562
|
+
strategyName: STRATEGY_NAME,
|
|
563
|
+
action: "penalized",
|
|
564
|
+
score: newScore,
|
|
565
|
+
reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
|
|
566
|
+
}
|
|
567
|
+
]
|
|
568
|
+
});
|
|
569
|
+
} else {
|
|
570
|
+
out.push(picked);
|
|
571
|
+
}
|
|
572
|
+
for (const tag of picked.tags ?? []) {
|
|
573
|
+
emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return out;
|
|
577
|
+
}
|
|
578
|
+
var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
|
|
579
|
+
var init_diversityRerank = __esm({
|
|
580
|
+
"src/core/navigators/diversityRerank.ts"() {
|
|
581
|
+
"use strict";
|
|
582
|
+
DIVERSITY_STRENGTH = 0.6;
|
|
583
|
+
DIVERSITY_FLOOR = 0.3;
|
|
584
|
+
STRATEGY = "diversityRerank";
|
|
585
|
+
STRATEGY_ID = "DIVERSITY_RERANK";
|
|
586
|
+
STRATEGY_NAME = "Diversity Re-rank";
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
501
590
|
// src/core/navigators/PipelineDebugger.ts
|
|
502
591
|
var PipelineDebugger_exports = {};
|
|
503
592
|
__export(PipelineDebugger_exports, {
|
|
504
593
|
buildRunReport: () => buildRunReport,
|
|
505
594
|
captureRun: () => captureRun,
|
|
506
595
|
clearRunHistory: () => clearRunHistory,
|
|
596
|
+
getActivePipeline: () => getActivePipeline,
|
|
507
597
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
508
598
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
509
599
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -511,6 +601,9 @@ __export(PipelineDebugger_exports, {
|
|
|
511
601
|
function registerPipelineForDebug(pipeline) {
|
|
512
602
|
_activePipeline = pipeline;
|
|
513
603
|
}
|
|
604
|
+
function getActivePipeline() {
|
|
605
|
+
return _activePipeline;
|
|
606
|
+
}
|
|
514
607
|
function clearRunHistory() {
|
|
515
608
|
runHistory.length = 0;
|
|
516
609
|
}
|
|
@@ -1693,7 +1786,7 @@ function shuffleInPlace(arr) {
|
|
|
1693
1786
|
function pickTopByScore(cards, limit) {
|
|
1694
1787
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1695
1788
|
}
|
|
1696
|
-
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;
|
|
1789
|
+
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;
|
|
1697
1790
|
var init_prescribed = __esm({
|
|
1698
1791
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1699
1792
|
"use strict";
|
|
@@ -1704,9 +1797,12 @@ var init_prescribed = __esm({
|
|
|
1704
1797
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1705
1798
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1706
1799
|
DEFAULT_MIN_COUNT = 3;
|
|
1800
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
1801
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
1707
1802
|
BASE_TARGET_SCORE = 1;
|
|
1708
1803
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1709
1804
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
1805
|
+
BASE_PRACTICE_SCORE = 1;
|
|
1710
1806
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1711
1807
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1712
1808
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -1814,7 +1910,18 @@ var init_prescribed = __esm({
|
|
|
1814
1910
|
courseId,
|
|
1815
1911
|
emittedIds
|
|
1816
1912
|
);
|
|
1817
|
-
|
|
1913
|
+
const practiceCards = this.buildPracticeCards({
|
|
1914
|
+
group,
|
|
1915
|
+
courseId,
|
|
1916
|
+
emittedIds,
|
|
1917
|
+
cardsByTag,
|
|
1918
|
+
hierarchyConfigs,
|
|
1919
|
+
userTagElo,
|
|
1920
|
+
userGlobalElo,
|
|
1921
|
+
activeIds,
|
|
1922
|
+
seenIds
|
|
1923
|
+
});
|
|
1924
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
1818
1925
|
}
|
|
1819
1926
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
1820
1927
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -1842,6 +1949,7 @@ var init_prescribed = __esm({
|
|
|
1842
1949
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
1843
1950
|
for (const card of finalCards) {
|
|
1844
1951
|
const prov = card.provenance[0];
|
|
1952
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
1845
1953
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
1846
1954
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
1847
1955
|
if (!groupId) continue;
|
|
@@ -1911,7 +2019,12 @@ var init_prescribed = __esm({
|
|
|
1911
2019
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
1912
2020
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
1913
2021
|
},
|
|
1914
|
-
retireOnEncounter: raw.retireOnEncounter !== false
|
|
2022
|
+
retireOnEncounter: raw.retireOnEncounter !== false,
|
|
2023
|
+
practiceTagPatterns: dedupe(
|
|
2024
|
+
Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
|
|
2025
|
+
),
|
|
2026
|
+
practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
|
|
2027
|
+
maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
|
|
1915
2028
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
1916
2029
|
return { groups };
|
|
1917
2030
|
} catch {
|
|
@@ -2134,6 +2247,92 @@ var init_prescribed = __esm({
|
|
|
2134
2247
|
}
|
|
2135
2248
|
return cards;
|
|
2136
2249
|
}
|
|
2250
|
+
/**
|
|
2251
|
+
* Emit drill cards for *unlocked-but-under-practiced* skills.
|
|
2252
|
+
*
|
|
2253
|
+
* For each course tag matching the group's `practiceTagPatterns` that is both
|
|
2254
|
+
* unlocked (all hierarchy prerequisites met — i.e. the learner has been
|
|
2255
|
+
* introduced to it) and under-practiced (per-tag attempt count below
|
|
2256
|
+
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2257
|
+
* into the candidate pool. It exists because global-ELO retrieval
|
|
2258
|
+
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2259
|
+
* freshly-introduced skill — putting them in the pool here lets the pipeline's
|
|
2260
|
+
* scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
|
|
2261
|
+
* this method's job; it only guarantees presence.
|
|
2262
|
+
*
|
|
2263
|
+
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2264
|
+
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
2265
|
+
*/
|
|
2266
|
+
buildPracticeCards(args) {
|
|
2267
|
+
const {
|
|
2268
|
+
group,
|
|
2269
|
+
courseId,
|
|
2270
|
+
emittedIds,
|
|
2271
|
+
cardsByTag,
|
|
2272
|
+
hierarchyConfigs,
|
|
2273
|
+
userTagElo,
|
|
2274
|
+
userGlobalElo,
|
|
2275
|
+
activeIds,
|
|
2276
|
+
seenIds
|
|
2277
|
+
} = args;
|
|
2278
|
+
const patterns = group.practiceTagPatterns ?? [];
|
|
2279
|
+
if (patterns.length === 0) return [];
|
|
2280
|
+
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2281
|
+
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2282
|
+
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2283
|
+
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2284
|
+
);
|
|
2285
|
+
if (practiceTags.length === 0) return [];
|
|
2286
|
+
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2287
|
+
supportTags: practiceTags,
|
|
2288
|
+
cardsByTag,
|
|
2289
|
+
activeIds,
|
|
2290
|
+
seenIds,
|
|
2291
|
+
excludedIds: emittedIds,
|
|
2292
|
+
limit: maxPractice
|
|
2293
|
+
});
|
|
2294
|
+
if (practiceCardIds.length === 0) return [];
|
|
2295
|
+
logger.info(
|
|
2296
|
+
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2297
|
+
);
|
|
2298
|
+
const cards = [];
|
|
2299
|
+
for (const cardId of practiceCardIds) {
|
|
2300
|
+
emittedIds.add(cardId);
|
|
2301
|
+
cards.push({
|
|
2302
|
+
cardId,
|
|
2303
|
+
courseId,
|
|
2304
|
+
score: BASE_PRACTICE_SCORE,
|
|
2305
|
+
provenance: [
|
|
2306
|
+
{
|
|
2307
|
+
strategy: "prescribed",
|
|
2308
|
+
strategyName: this.strategyName || this.name,
|
|
2309
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2310
|
+
action: "generated",
|
|
2311
|
+
score: BASE_PRACTICE_SCORE,
|
|
2312
|
+
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2313
|
+
}
|
|
2314
|
+
]
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
return cards;
|
|
2318
|
+
}
|
|
2319
|
+
/**
|
|
2320
|
+
* True for a skill that was *gated and is now reached*: it has at least one
|
|
2321
|
+
* declared hierarchy prerequisite set, and every set is fully satisfied by the
|
|
2322
|
+
* learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
|
|
2323
|
+
* — an ungated tag was never "introduced" in the curricular sense, so it isn't
|
|
2324
|
+
* a post-intro drill target (e.g. whole-word spelling tags that share the
|
|
2325
|
+
* `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
|
|
2326
|
+
* ELO retrieval. This is the precise population the retrieval gap strands:
|
|
2327
|
+
* just-unlocked, low-ELO skills.
|
|
2328
|
+
*/
|
|
2329
|
+
isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
|
|
2330
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
2331
|
+
if (prereqSets.length === 0) return false;
|
|
2332
|
+
return prereqSets.every(
|
|
2333
|
+
(prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
2334
|
+
);
|
|
2335
|
+
}
|
|
2137
2336
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2138
2337
|
if (supportTags.length === 0) {
|
|
2139
2338
|
return [];
|
|
@@ -3682,7 +3881,7 @@ function logResultCards(cards) {
|
|
|
3682
3881
|
for (let i = 0; i < cards.length; i++) {
|
|
3683
3882
|
const c = cards[i];
|
|
3684
3883
|
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
3685
|
-
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
3884
|
+
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) => {
|
|
3686
3885
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
3687
3886
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
3688
3887
|
}).join(" | ");
|
|
@@ -3713,6 +3912,7 @@ var init_Pipeline = __esm({
|
|
|
3713
3912
|
init_logger();
|
|
3714
3913
|
init_orchestration();
|
|
3715
3914
|
init_PipelineDebugger();
|
|
3915
|
+
init_diversityRerank();
|
|
3716
3916
|
VERBOSE_RESULTS = true;
|
|
3717
3917
|
Pipeline = class extends ContentNavigator {
|
|
3718
3918
|
generator;
|
|
@@ -3886,6 +4086,7 @@ var init_Pipeline = __esm({
|
|
|
3886
4086
|
this._ephemeralHints = null;
|
|
3887
4087
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
3888
4088
|
}
|
|
4089
|
+
cards = diversityRerank(cards);
|
|
3889
4090
|
cards.sort((a, b) => b.score - a.score);
|
|
3890
4091
|
const tFilter = performance.now();
|
|
3891
4092
|
const result = cards.slice(0, limit);
|
|
@@ -4189,6 +4390,68 @@ var init_Pipeline = __esm({
|
|
|
4189
4390
|
// ---------------------------------------------------------------------------
|
|
4190
4391
|
// Card-space diagnostic
|
|
4191
4392
|
// ---------------------------------------------------------------------------
|
|
4393
|
+
/**
|
|
4394
|
+
* Commit-free forecast: score the user's full card space through the filter
|
|
4395
|
+
* chain and return the cards that are currently *reachable* (score >=
|
|
4396
|
+
* threshold), optionally nudged by caller-supplied hints and/or restricted
|
|
4397
|
+
* to cards the user hasn't seen yet.
|
|
4398
|
+
*
|
|
4399
|
+
* This is a GENERIC primitive — it returns scored, tag-hydrated cards and
|
|
4400
|
+
* stops there. It has no knowledge of any particular tag convention; callers
|
|
4401
|
+
* decide what the surviving cards mean (e.g. filter to their own "intro"
|
|
4402
|
+
* tag family). Nothing is written and no session is started.
|
|
4403
|
+
*
|
|
4404
|
+
* The optional `hints` are the "out-of-band kick": they run through the same
|
|
4405
|
+
* {@link applyHints} path a live replan uses, so the two semantics carry over —
|
|
4406
|
+
* - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
|
|
4407
|
+
* stays out), and
|
|
4408
|
+
* - `requireTags`/`requireCards` inject from the full pre-filter pool,
|
|
4409
|
+
* *bypassing* gating (use when you want a card regardless of reachability).
|
|
4410
|
+
* Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
|
|
4411
|
+
* user has already seen — pass `unseenOnly: false` if that matters.
|
|
4412
|
+
*
|
|
4413
|
+
* Cost note: like {@link diagnoseCardSpace}, this scans every card through the
|
|
4414
|
+
* filters, so it's heavier than a normal replan. Intended for one-shot
|
|
4415
|
+
* out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
|
|
4416
|
+
*
|
|
4417
|
+
* @param opts.hints Optional ephemeral hints to apply after the filter chain.
|
|
4418
|
+
* @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
|
|
4419
|
+
* @param opts.threshold Min score to count as reachable (default 0.10).
|
|
4420
|
+
* @param opts.limit Optional cap on results (already sorted desc).
|
|
4421
|
+
*/
|
|
4422
|
+
async forecast(opts) {
|
|
4423
|
+
const threshold = opts?.threshold ?? 0.1;
|
|
4424
|
+
const unseenOnly = opts?.unseenOnly ?? true;
|
|
4425
|
+
const courseId = this.course.getCourseID();
|
|
4426
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
4427
|
+
let cards = allCardIds.map((cardId) => ({
|
|
4428
|
+
cardId,
|
|
4429
|
+
courseId,
|
|
4430
|
+
score: 1,
|
|
4431
|
+
provenance: []
|
|
4432
|
+
}));
|
|
4433
|
+
cards = await this.hydrateTags(cards);
|
|
4434
|
+
const fullPool = cards.slice();
|
|
4435
|
+
const context = await this.buildContext();
|
|
4436
|
+
for (const filter of this.filters) {
|
|
4437
|
+
cards = await filter.transform(cards, context);
|
|
4438
|
+
}
|
|
4439
|
+
if (opts?.hints) {
|
|
4440
|
+
cards = this.applyHints(cards, opts.hints, fullPool);
|
|
4441
|
+
}
|
|
4442
|
+
cards = cards.filter((c) => c.score >= threshold);
|
|
4443
|
+
if (unseenOnly) {
|
|
4444
|
+
let encountered;
|
|
4445
|
+
try {
|
|
4446
|
+
encountered = new Set(await this.user.getSeenCards(courseId));
|
|
4447
|
+
} catch {
|
|
4448
|
+
encountered = /* @__PURE__ */ new Set();
|
|
4449
|
+
}
|
|
4450
|
+
cards = cards.filter((c) => !encountered.has(c.cardId));
|
|
4451
|
+
}
|
|
4452
|
+
cards.sort((a, b) => b.score - a.score);
|
|
4453
|
+
return opts?.limit ? cards.slice(0, opts.limit) : cards;
|
|
4454
|
+
}
|
|
4192
4455
|
/**
|
|
4193
4456
|
* Scan every card in the course through the filter chain and report
|
|
4194
4457
|
* how many are "well indicated" (score >= threshold) for the current user.
|
|
@@ -4454,6 +4717,7 @@ var init_3 = __esm({
|
|
|
4454
4717
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4455
4718
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4456
4719
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
4720
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4457
4721
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
4458
4722
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
4459
4723
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -4479,9 +4743,13 @@ var init_3 = __esm({
|
|
|
4479
4743
|
var navigators_exports = {};
|
|
4480
4744
|
__export(navigators_exports, {
|
|
4481
4745
|
ContentNavigator: () => ContentNavigator,
|
|
4746
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
4747
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
4482
4748
|
NavigatorRole: () => NavigatorRole,
|
|
4483
4749
|
NavigatorRoles: () => NavigatorRoles,
|
|
4484
4750
|
Navigators: () => Navigators,
|
|
4751
|
+
diversityRerank: () => diversityRerank,
|
|
4752
|
+
getActivePipeline: () => getActivePipeline,
|
|
4485
4753
|
getCardOrigin: () => getCardOrigin,
|
|
4486
4754
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
4487
4755
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -4565,6 +4833,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
4565
4833
|
var init_navigators = __esm({
|
|
4566
4834
|
"src/core/navigators/index.ts"() {
|
|
4567
4835
|
"use strict";
|
|
4836
|
+
init_diversityRerank();
|
|
4568
4837
|
init_PipelineDebugger();
|
|
4569
4838
|
init_logger();
|
|
4570
4839
|
init_();
|