@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
|
@@ -621,12 +621,102 @@ var init_courseLookupDB = __esm({
|
|
|
621
621
|
}
|
|
622
622
|
});
|
|
623
623
|
|
|
624
|
+
// src/core/navigators/diversityRerank.ts
|
|
625
|
+
var diversityRerank_exports = {};
|
|
626
|
+
__export(diversityRerank_exports, {
|
|
627
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
628
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
629
|
+
diversityRerank: () => diversityRerank
|
|
630
|
+
});
|
|
631
|
+
function diversityRerank(cards, opts = {}) {
|
|
632
|
+
const strength = opts.strength ?? DIVERSITY_STRENGTH;
|
|
633
|
+
const floor = opts.floor ?? DIVERSITY_FLOOR;
|
|
634
|
+
const n = cards.length;
|
|
635
|
+
if (n <= 1) return cards;
|
|
636
|
+
const df = /* @__PURE__ */ new Map();
|
|
637
|
+
for (const card of cards) {
|
|
638
|
+
for (const tag of card.tags ?? []) {
|
|
639
|
+
df.set(tag, (df.get(tag) ?? 0) + 1);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
const idf = /* @__PURE__ */ new Map();
|
|
643
|
+
for (const [tag, freq] of df) {
|
|
644
|
+
idf.set(tag, Math.log(n / freq));
|
|
645
|
+
}
|
|
646
|
+
const remaining = [...cards];
|
|
647
|
+
const emittedCount = /* @__PURE__ */ new Map();
|
|
648
|
+
const out = [];
|
|
649
|
+
const repetitionLoad = (card) => {
|
|
650
|
+
let load = 0;
|
|
651
|
+
for (const tag of card.tags ?? []) {
|
|
652
|
+
const seen = emittedCount.get(tag);
|
|
653
|
+
if (seen) load += (idf.get(tag) ?? 0) * seen;
|
|
654
|
+
}
|
|
655
|
+
return load;
|
|
656
|
+
};
|
|
657
|
+
while (remaining.length > 0) {
|
|
658
|
+
let bestIdx = 0;
|
|
659
|
+
let bestValue = -Infinity;
|
|
660
|
+
let bestPenalty = 1;
|
|
661
|
+
let bestLoad = 0;
|
|
662
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
663
|
+
const card = remaining[i];
|
|
664
|
+
const load = repetitionLoad(card);
|
|
665
|
+
const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
|
|
666
|
+
const value = card.score * penalty;
|
|
667
|
+
if (value > bestValue) {
|
|
668
|
+
bestValue = value;
|
|
669
|
+
bestIdx = i;
|
|
670
|
+
bestPenalty = penalty;
|
|
671
|
+
bestLoad = load;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
const [picked] = remaining.splice(bestIdx, 1);
|
|
675
|
+
if (Number.isFinite(picked.score) && bestPenalty < 1) {
|
|
676
|
+
const newScore = picked.score * bestPenalty;
|
|
677
|
+
out.push({
|
|
678
|
+
...picked,
|
|
679
|
+
score: newScore,
|
|
680
|
+
provenance: [
|
|
681
|
+
...picked.provenance,
|
|
682
|
+
{
|
|
683
|
+
strategy: STRATEGY,
|
|
684
|
+
strategyId: STRATEGY_ID,
|
|
685
|
+
strategyName: STRATEGY_NAME,
|
|
686
|
+
action: "penalized",
|
|
687
|
+
score: newScore,
|
|
688
|
+
reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
|
|
689
|
+
}
|
|
690
|
+
]
|
|
691
|
+
});
|
|
692
|
+
} else {
|
|
693
|
+
out.push(picked);
|
|
694
|
+
}
|
|
695
|
+
for (const tag of picked.tags ?? []) {
|
|
696
|
+
emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
return out;
|
|
700
|
+
}
|
|
701
|
+
var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
|
|
702
|
+
var init_diversityRerank = __esm({
|
|
703
|
+
"src/core/navigators/diversityRerank.ts"() {
|
|
704
|
+
"use strict";
|
|
705
|
+
DIVERSITY_STRENGTH = 0.6;
|
|
706
|
+
DIVERSITY_FLOOR = 0.3;
|
|
707
|
+
STRATEGY = "diversityRerank";
|
|
708
|
+
STRATEGY_ID = "DIVERSITY_RERANK";
|
|
709
|
+
STRATEGY_NAME = "Diversity Re-rank";
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
624
713
|
// src/core/navigators/PipelineDebugger.ts
|
|
625
714
|
var PipelineDebugger_exports = {};
|
|
626
715
|
__export(PipelineDebugger_exports, {
|
|
627
716
|
buildRunReport: () => buildRunReport,
|
|
628
717
|
captureRun: () => captureRun,
|
|
629
718
|
clearRunHistory: () => clearRunHistory,
|
|
719
|
+
getActivePipeline: () => getActivePipeline,
|
|
630
720
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
631
721
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
632
722
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -634,6 +724,9 @@ __export(PipelineDebugger_exports, {
|
|
|
634
724
|
function registerPipelineForDebug(pipeline) {
|
|
635
725
|
_activePipeline = pipeline;
|
|
636
726
|
}
|
|
727
|
+
function getActivePipeline() {
|
|
728
|
+
return _activePipeline;
|
|
729
|
+
}
|
|
637
730
|
function clearRunHistory() {
|
|
638
731
|
runHistory.length = 0;
|
|
639
732
|
}
|
|
@@ -1816,7 +1909,7 @@ function shuffleInPlace(arr) {
|
|
|
1816
1909
|
function pickTopByScore(cards, limit) {
|
|
1817
1910
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1818
1911
|
}
|
|
1819
|
-
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;
|
|
1912
|
+
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;
|
|
1820
1913
|
var init_prescribed = __esm({
|
|
1821
1914
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1822
1915
|
"use strict";
|
|
@@ -1827,9 +1920,12 @@ var init_prescribed = __esm({
|
|
|
1827
1920
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1828
1921
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1829
1922
|
DEFAULT_MIN_COUNT = 3;
|
|
1923
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
1924
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
1830
1925
|
BASE_TARGET_SCORE = 1;
|
|
1831
1926
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1832
1927
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
1928
|
+
BASE_PRACTICE_SCORE = 1;
|
|
1833
1929
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1834
1930
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1835
1931
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -1937,7 +2033,18 @@ var init_prescribed = __esm({
|
|
|
1937
2033
|
courseId,
|
|
1938
2034
|
emittedIds
|
|
1939
2035
|
);
|
|
1940
|
-
|
|
2036
|
+
const practiceCards = this.buildPracticeCards({
|
|
2037
|
+
group,
|
|
2038
|
+
courseId,
|
|
2039
|
+
emittedIds,
|
|
2040
|
+
cardsByTag,
|
|
2041
|
+
hierarchyConfigs,
|
|
2042
|
+
userTagElo,
|
|
2043
|
+
userGlobalElo,
|
|
2044
|
+
activeIds,
|
|
2045
|
+
seenIds
|
|
2046
|
+
});
|
|
2047
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
1941
2048
|
}
|
|
1942
2049
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
1943
2050
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -1965,6 +2072,7 @@ var init_prescribed = __esm({
|
|
|
1965
2072
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
1966
2073
|
for (const card of finalCards) {
|
|
1967
2074
|
const prov = card.provenance[0];
|
|
2075
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
1968
2076
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
1969
2077
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
1970
2078
|
if (!groupId) continue;
|
|
@@ -2034,7 +2142,12 @@ var init_prescribed = __esm({
|
|
|
2034
2142
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
2035
2143
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
2036
2144
|
},
|
|
2037
|
-
retireOnEncounter: raw.retireOnEncounter !== false
|
|
2145
|
+
retireOnEncounter: raw.retireOnEncounter !== false,
|
|
2146
|
+
practiceTagPatterns: dedupe(
|
|
2147
|
+
Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
|
|
2148
|
+
),
|
|
2149
|
+
practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
|
|
2150
|
+
maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
|
|
2038
2151
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
2039
2152
|
return { groups };
|
|
2040
2153
|
} catch {
|
|
@@ -2257,6 +2370,92 @@ var init_prescribed = __esm({
|
|
|
2257
2370
|
}
|
|
2258
2371
|
return cards;
|
|
2259
2372
|
}
|
|
2373
|
+
/**
|
|
2374
|
+
* Emit drill cards for *unlocked-but-under-practiced* skills.
|
|
2375
|
+
*
|
|
2376
|
+
* For each course tag matching the group's `practiceTagPatterns` that is both
|
|
2377
|
+
* unlocked (all hierarchy prerequisites met — i.e. the learner has been
|
|
2378
|
+
* introduced to it) and under-practiced (per-tag attempt count below
|
|
2379
|
+
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2380
|
+
* into the candidate pool. It exists because global-ELO retrieval
|
|
2381
|
+
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2382
|
+
* freshly-introduced skill — putting them in the pool here lets the pipeline's
|
|
2383
|
+
* scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
|
|
2384
|
+
* this method's job; it only guarantees presence.
|
|
2385
|
+
*
|
|
2386
|
+
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2387
|
+
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
2388
|
+
*/
|
|
2389
|
+
buildPracticeCards(args) {
|
|
2390
|
+
const {
|
|
2391
|
+
group,
|
|
2392
|
+
courseId,
|
|
2393
|
+
emittedIds,
|
|
2394
|
+
cardsByTag,
|
|
2395
|
+
hierarchyConfigs,
|
|
2396
|
+
userTagElo,
|
|
2397
|
+
userGlobalElo,
|
|
2398
|
+
activeIds,
|
|
2399
|
+
seenIds
|
|
2400
|
+
} = args;
|
|
2401
|
+
const patterns = group.practiceTagPatterns ?? [];
|
|
2402
|
+
if (patterns.length === 0) return [];
|
|
2403
|
+
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2404
|
+
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2405
|
+
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2406
|
+
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2407
|
+
);
|
|
2408
|
+
if (practiceTags.length === 0) return [];
|
|
2409
|
+
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2410
|
+
supportTags: practiceTags,
|
|
2411
|
+
cardsByTag,
|
|
2412
|
+
activeIds,
|
|
2413
|
+
seenIds,
|
|
2414
|
+
excludedIds: emittedIds,
|
|
2415
|
+
limit: maxPractice
|
|
2416
|
+
});
|
|
2417
|
+
if (practiceCardIds.length === 0) return [];
|
|
2418
|
+
logger.info(
|
|
2419
|
+
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2420
|
+
);
|
|
2421
|
+
const cards = [];
|
|
2422
|
+
for (const cardId of practiceCardIds) {
|
|
2423
|
+
emittedIds.add(cardId);
|
|
2424
|
+
cards.push({
|
|
2425
|
+
cardId,
|
|
2426
|
+
courseId,
|
|
2427
|
+
score: BASE_PRACTICE_SCORE,
|
|
2428
|
+
provenance: [
|
|
2429
|
+
{
|
|
2430
|
+
strategy: "prescribed",
|
|
2431
|
+
strategyName: this.strategyName || this.name,
|
|
2432
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2433
|
+
action: "generated",
|
|
2434
|
+
score: BASE_PRACTICE_SCORE,
|
|
2435
|
+
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2436
|
+
}
|
|
2437
|
+
]
|
|
2438
|
+
});
|
|
2439
|
+
}
|
|
2440
|
+
return cards;
|
|
2441
|
+
}
|
|
2442
|
+
/**
|
|
2443
|
+
* True for a skill that was *gated and is now reached*: it has at least one
|
|
2444
|
+
* declared hierarchy prerequisite set, and every set is fully satisfied by the
|
|
2445
|
+
* learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
|
|
2446
|
+
* — an ungated tag was never "introduced" in the curricular sense, so it isn't
|
|
2447
|
+
* a post-intro drill target (e.g. whole-word spelling tags that share the
|
|
2448
|
+
* `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
|
|
2449
|
+
* ELO retrieval. This is the precise population the retrieval gap strands:
|
|
2450
|
+
* just-unlocked, low-ELO skills.
|
|
2451
|
+
*/
|
|
2452
|
+
isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
|
|
2453
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
2454
|
+
if (prereqSets.length === 0) return false;
|
|
2455
|
+
return prereqSets.every(
|
|
2456
|
+
(prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
2457
|
+
);
|
|
2458
|
+
}
|
|
2260
2459
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2261
2460
|
if (supportTags.length === 0) {
|
|
2262
2461
|
return [];
|
|
@@ -3805,7 +4004,7 @@ function logResultCards(cards) {
|
|
|
3805
4004
|
for (let i = 0; i < cards.length; i++) {
|
|
3806
4005
|
const c = cards[i];
|
|
3807
4006
|
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
3808
|
-
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
4007
|
+
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) => {
|
|
3809
4008
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
3810
4009
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
3811
4010
|
}).join(" | ");
|
|
@@ -3836,6 +4035,7 @@ var init_Pipeline = __esm({
|
|
|
3836
4035
|
init_logger();
|
|
3837
4036
|
init_orchestration();
|
|
3838
4037
|
init_PipelineDebugger();
|
|
4038
|
+
init_diversityRerank();
|
|
3839
4039
|
VERBOSE_RESULTS = true;
|
|
3840
4040
|
Pipeline = class extends ContentNavigator {
|
|
3841
4041
|
generator;
|
|
@@ -4009,6 +4209,7 @@ var init_Pipeline = __esm({
|
|
|
4009
4209
|
this._ephemeralHints = null;
|
|
4010
4210
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
4011
4211
|
}
|
|
4212
|
+
cards = diversityRerank(cards);
|
|
4012
4213
|
cards.sort((a, b) => b.score - a.score);
|
|
4013
4214
|
const tFilter = performance.now();
|
|
4014
4215
|
const result = cards.slice(0, limit);
|
|
@@ -4312,6 +4513,68 @@ var init_Pipeline = __esm({
|
|
|
4312
4513
|
// ---------------------------------------------------------------------------
|
|
4313
4514
|
// Card-space diagnostic
|
|
4314
4515
|
// ---------------------------------------------------------------------------
|
|
4516
|
+
/**
|
|
4517
|
+
* Commit-free forecast: score the user's full card space through the filter
|
|
4518
|
+
* chain and return the cards that are currently *reachable* (score >=
|
|
4519
|
+
* threshold), optionally nudged by caller-supplied hints and/or restricted
|
|
4520
|
+
* to cards the user hasn't seen yet.
|
|
4521
|
+
*
|
|
4522
|
+
* This is a GENERIC primitive — it returns scored, tag-hydrated cards and
|
|
4523
|
+
* stops there. It has no knowledge of any particular tag convention; callers
|
|
4524
|
+
* decide what the surviving cards mean (e.g. filter to their own "intro"
|
|
4525
|
+
* tag family). Nothing is written and no session is started.
|
|
4526
|
+
*
|
|
4527
|
+
* The optional `hints` are the "out-of-band kick": they run through the same
|
|
4528
|
+
* {@link applyHints} path a live replan uses, so the two semantics carry over —
|
|
4529
|
+
* - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
|
|
4530
|
+
* stays out), and
|
|
4531
|
+
* - `requireTags`/`requireCards` inject from the full pre-filter pool,
|
|
4532
|
+
* *bypassing* gating (use when you want a card regardless of reachability).
|
|
4533
|
+
* Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
|
|
4534
|
+
* user has already seen — pass `unseenOnly: false` if that matters.
|
|
4535
|
+
*
|
|
4536
|
+
* Cost note: like {@link diagnoseCardSpace}, this scans every card through the
|
|
4537
|
+
* filters, so it's heavier than a normal replan. Intended for one-shot
|
|
4538
|
+
* out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
|
|
4539
|
+
*
|
|
4540
|
+
* @param opts.hints Optional ephemeral hints to apply after the filter chain.
|
|
4541
|
+
* @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
|
|
4542
|
+
* @param opts.threshold Min score to count as reachable (default 0.10).
|
|
4543
|
+
* @param opts.limit Optional cap on results (already sorted desc).
|
|
4544
|
+
*/
|
|
4545
|
+
async forecast(opts) {
|
|
4546
|
+
const threshold = opts?.threshold ?? 0.1;
|
|
4547
|
+
const unseenOnly = opts?.unseenOnly ?? true;
|
|
4548
|
+
const courseId = this.course.getCourseID();
|
|
4549
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
4550
|
+
let cards = allCardIds.map((cardId) => ({
|
|
4551
|
+
cardId,
|
|
4552
|
+
courseId,
|
|
4553
|
+
score: 1,
|
|
4554
|
+
provenance: []
|
|
4555
|
+
}));
|
|
4556
|
+
cards = await this.hydrateTags(cards);
|
|
4557
|
+
const fullPool = cards.slice();
|
|
4558
|
+
const context = await this.buildContext();
|
|
4559
|
+
for (const filter of this.filters) {
|
|
4560
|
+
cards = await filter.transform(cards, context);
|
|
4561
|
+
}
|
|
4562
|
+
if (opts?.hints) {
|
|
4563
|
+
cards = this.applyHints(cards, opts.hints, fullPool);
|
|
4564
|
+
}
|
|
4565
|
+
cards = cards.filter((c) => c.score >= threshold);
|
|
4566
|
+
if (unseenOnly) {
|
|
4567
|
+
let encountered;
|
|
4568
|
+
try {
|
|
4569
|
+
encountered = new Set(await this.user.getSeenCards(courseId));
|
|
4570
|
+
} catch {
|
|
4571
|
+
encountered = /* @__PURE__ */ new Set();
|
|
4572
|
+
}
|
|
4573
|
+
cards = cards.filter((c) => !encountered.has(c.cardId));
|
|
4574
|
+
}
|
|
4575
|
+
cards.sort((a, b) => b.score - a.score);
|
|
4576
|
+
return opts?.limit ? cards.slice(0, opts.limit) : cards;
|
|
4577
|
+
}
|
|
4315
4578
|
/**
|
|
4316
4579
|
* Scan every card in the course through the filter chain and report
|
|
4317
4580
|
* how many are "well indicated" (score >= threshold) for the current user.
|
|
@@ -4577,6 +4840,7 @@ var init_3 = __esm({
|
|
|
4577
4840
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4578
4841
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4579
4842
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
4843
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4580
4844
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
4581
4845
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
4582
4846
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -4602,9 +4866,13 @@ var init_3 = __esm({
|
|
|
4602
4866
|
var navigators_exports = {};
|
|
4603
4867
|
__export(navigators_exports, {
|
|
4604
4868
|
ContentNavigator: () => ContentNavigator,
|
|
4869
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
4870
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
4605
4871
|
NavigatorRole: () => NavigatorRole,
|
|
4606
4872
|
NavigatorRoles: () => NavigatorRoles,
|
|
4607
4873
|
Navigators: () => Navigators,
|
|
4874
|
+
diversityRerank: () => diversityRerank,
|
|
4875
|
+
getActivePipeline: () => getActivePipeline,
|
|
4608
4876
|
getCardOrigin: () => getCardOrigin,
|
|
4609
4877
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
4610
4878
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -4688,6 +4956,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
4688
4956
|
var init_navigators = __esm({
|
|
4689
4957
|
"src/core/navigators/index.ts"() {
|
|
4690
4958
|
"use strict";
|
|
4959
|
+
init_diversityRerank();
|
|
4691
4960
|
init_PipelineDebugger();
|
|
4692
4961
|
init_logger();
|
|
4693
4962
|
init_();
|