@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
package/dist/core/index.mjs
CHANGED
|
@@ -696,12 +696,102 @@ var init_courseLookupDB = __esm({
|
|
|
696
696
|
}
|
|
697
697
|
});
|
|
698
698
|
|
|
699
|
+
// src/core/navigators/diversityRerank.ts
|
|
700
|
+
var diversityRerank_exports = {};
|
|
701
|
+
__export(diversityRerank_exports, {
|
|
702
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
703
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
704
|
+
diversityRerank: () => diversityRerank
|
|
705
|
+
});
|
|
706
|
+
function diversityRerank(cards, opts = {}) {
|
|
707
|
+
const strength = opts.strength ?? DIVERSITY_STRENGTH;
|
|
708
|
+
const floor = opts.floor ?? DIVERSITY_FLOOR;
|
|
709
|
+
const n = cards.length;
|
|
710
|
+
if (n <= 1) return cards;
|
|
711
|
+
const df = /* @__PURE__ */ new Map();
|
|
712
|
+
for (const card of cards) {
|
|
713
|
+
for (const tag of card.tags ?? []) {
|
|
714
|
+
df.set(tag, (df.get(tag) ?? 0) + 1);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
const idf = /* @__PURE__ */ new Map();
|
|
718
|
+
for (const [tag, freq] of df) {
|
|
719
|
+
idf.set(tag, Math.log(n / freq));
|
|
720
|
+
}
|
|
721
|
+
const remaining = [...cards];
|
|
722
|
+
const emittedCount = /* @__PURE__ */ new Map();
|
|
723
|
+
const out = [];
|
|
724
|
+
const repetitionLoad = (card) => {
|
|
725
|
+
let load = 0;
|
|
726
|
+
for (const tag of card.tags ?? []) {
|
|
727
|
+
const seen = emittedCount.get(tag);
|
|
728
|
+
if (seen) load += (idf.get(tag) ?? 0) * seen;
|
|
729
|
+
}
|
|
730
|
+
return load;
|
|
731
|
+
};
|
|
732
|
+
while (remaining.length > 0) {
|
|
733
|
+
let bestIdx = 0;
|
|
734
|
+
let bestValue = -Infinity;
|
|
735
|
+
let bestPenalty = 1;
|
|
736
|
+
let bestLoad = 0;
|
|
737
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
738
|
+
const card = remaining[i];
|
|
739
|
+
const load = repetitionLoad(card);
|
|
740
|
+
const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
|
|
741
|
+
const value = card.score * penalty;
|
|
742
|
+
if (value > bestValue) {
|
|
743
|
+
bestValue = value;
|
|
744
|
+
bestIdx = i;
|
|
745
|
+
bestPenalty = penalty;
|
|
746
|
+
bestLoad = load;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
const [picked] = remaining.splice(bestIdx, 1);
|
|
750
|
+
if (Number.isFinite(picked.score) && bestPenalty < 1) {
|
|
751
|
+
const newScore = picked.score * bestPenalty;
|
|
752
|
+
out.push({
|
|
753
|
+
...picked,
|
|
754
|
+
score: newScore,
|
|
755
|
+
provenance: [
|
|
756
|
+
...picked.provenance,
|
|
757
|
+
{
|
|
758
|
+
strategy: STRATEGY,
|
|
759
|
+
strategyId: STRATEGY_ID,
|
|
760
|
+
strategyName: STRATEGY_NAME,
|
|
761
|
+
action: "penalized",
|
|
762
|
+
score: newScore,
|
|
763
|
+
reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
|
|
764
|
+
}
|
|
765
|
+
]
|
|
766
|
+
});
|
|
767
|
+
} else {
|
|
768
|
+
out.push(picked);
|
|
769
|
+
}
|
|
770
|
+
for (const tag of picked.tags ?? []) {
|
|
771
|
+
emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
return out;
|
|
775
|
+
}
|
|
776
|
+
var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
|
|
777
|
+
var init_diversityRerank = __esm({
|
|
778
|
+
"src/core/navigators/diversityRerank.ts"() {
|
|
779
|
+
"use strict";
|
|
780
|
+
DIVERSITY_STRENGTH = 0.6;
|
|
781
|
+
DIVERSITY_FLOOR = 0.3;
|
|
782
|
+
STRATEGY = "diversityRerank";
|
|
783
|
+
STRATEGY_ID = "DIVERSITY_RERANK";
|
|
784
|
+
STRATEGY_NAME = "Diversity Re-rank";
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
|
|
699
788
|
// src/core/navigators/PipelineDebugger.ts
|
|
700
789
|
var PipelineDebugger_exports = {};
|
|
701
790
|
__export(PipelineDebugger_exports, {
|
|
702
791
|
buildRunReport: () => buildRunReport,
|
|
703
792
|
captureRun: () => captureRun,
|
|
704
793
|
clearRunHistory: () => clearRunHistory,
|
|
794
|
+
getActivePipeline: () => getActivePipeline,
|
|
705
795
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
706
796
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
707
797
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -709,6 +799,9 @@ __export(PipelineDebugger_exports, {
|
|
|
709
799
|
function registerPipelineForDebug(pipeline) {
|
|
710
800
|
_activePipeline = pipeline;
|
|
711
801
|
}
|
|
802
|
+
function getActivePipeline() {
|
|
803
|
+
return _activePipeline;
|
|
804
|
+
}
|
|
712
805
|
function clearRunHistory() {
|
|
713
806
|
runHistory.length = 0;
|
|
714
807
|
}
|
|
@@ -1891,7 +1984,7 @@ function shuffleInPlace(arr) {
|
|
|
1891
1984
|
function pickTopByScore(cards, limit) {
|
|
1892
1985
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1893
1986
|
}
|
|
1894
|
-
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;
|
|
1987
|
+
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;
|
|
1895
1988
|
var init_prescribed = __esm({
|
|
1896
1989
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1897
1990
|
"use strict";
|
|
@@ -1902,9 +1995,12 @@ var init_prescribed = __esm({
|
|
|
1902
1995
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1903
1996
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1904
1997
|
DEFAULT_MIN_COUNT = 3;
|
|
1998
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
1999
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
1905
2000
|
BASE_TARGET_SCORE = 1;
|
|
1906
2001
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1907
2002
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
2003
|
+
BASE_PRACTICE_SCORE = 1;
|
|
1908
2004
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1909
2005
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1910
2006
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -2012,7 +2108,18 @@ var init_prescribed = __esm({
|
|
|
2012
2108
|
courseId,
|
|
2013
2109
|
emittedIds
|
|
2014
2110
|
);
|
|
2015
|
-
|
|
2111
|
+
const practiceCards = this.buildPracticeCards({
|
|
2112
|
+
group,
|
|
2113
|
+
courseId,
|
|
2114
|
+
emittedIds,
|
|
2115
|
+
cardsByTag,
|
|
2116
|
+
hierarchyConfigs,
|
|
2117
|
+
userTagElo,
|
|
2118
|
+
userGlobalElo,
|
|
2119
|
+
activeIds,
|
|
2120
|
+
seenIds
|
|
2121
|
+
});
|
|
2122
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
2016
2123
|
}
|
|
2017
2124
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2018
2125
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -2040,6 +2147,7 @@ var init_prescribed = __esm({
|
|
|
2040
2147
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
2041
2148
|
for (const card of finalCards) {
|
|
2042
2149
|
const prov = card.provenance[0];
|
|
2150
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
2043
2151
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
2044
2152
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
2045
2153
|
if (!groupId) continue;
|
|
@@ -2109,7 +2217,12 @@ var init_prescribed = __esm({
|
|
|
2109
2217
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
2110
2218
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
2111
2219
|
},
|
|
2112
|
-
retireOnEncounter: raw.retireOnEncounter !== false
|
|
2220
|
+
retireOnEncounter: raw.retireOnEncounter !== false,
|
|
2221
|
+
practiceTagPatterns: dedupe(
|
|
2222
|
+
Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
|
|
2223
|
+
),
|
|
2224
|
+
practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
|
|
2225
|
+
maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
|
|
2113
2226
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
2114
2227
|
return { groups };
|
|
2115
2228
|
} catch {
|
|
@@ -2332,6 +2445,92 @@ var init_prescribed = __esm({
|
|
|
2332
2445
|
}
|
|
2333
2446
|
return cards;
|
|
2334
2447
|
}
|
|
2448
|
+
/**
|
|
2449
|
+
* Emit drill cards for *unlocked-but-under-practiced* skills.
|
|
2450
|
+
*
|
|
2451
|
+
* For each course tag matching the group's `practiceTagPatterns` that is both
|
|
2452
|
+
* unlocked (all hierarchy prerequisites met — i.e. the learner has been
|
|
2453
|
+
* introduced to it) and under-practiced (per-tag attempt count below
|
|
2454
|
+
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2455
|
+
* into the candidate pool. It exists because global-ELO retrieval
|
|
2456
|
+
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2457
|
+
* freshly-introduced skill — putting them in the pool here lets the pipeline's
|
|
2458
|
+
* scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
|
|
2459
|
+
* this method's job; it only guarantees presence.
|
|
2460
|
+
*
|
|
2461
|
+
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2462
|
+
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
2463
|
+
*/
|
|
2464
|
+
buildPracticeCards(args) {
|
|
2465
|
+
const {
|
|
2466
|
+
group,
|
|
2467
|
+
courseId,
|
|
2468
|
+
emittedIds,
|
|
2469
|
+
cardsByTag,
|
|
2470
|
+
hierarchyConfigs,
|
|
2471
|
+
userTagElo,
|
|
2472
|
+
userGlobalElo,
|
|
2473
|
+
activeIds,
|
|
2474
|
+
seenIds
|
|
2475
|
+
} = args;
|
|
2476
|
+
const patterns = group.practiceTagPatterns ?? [];
|
|
2477
|
+
if (patterns.length === 0) return [];
|
|
2478
|
+
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2479
|
+
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2480
|
+
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2481
|
+
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2482
|
+
);
|
|
2483
|
+
if (practiceTags.length === 0) return [];
|
|
2484
|
+
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2485
|
+
supportTags: practiceTags,
|
|
2486
|
+
cardsByTag,
|
|
2487
|
+
activeIds,
|
|
2488
|
+
seenIds,
|
|
2489
|
+
excludedIds: emittedIds,
|
|
2490
|
+
limit: maxPractice
|
|
2491
|
+
});
|
|
2492
|
+
if (practiceCardIds.length === 0) return [];
|
|
2493
|
+
logger.info(
|
|
2494
|
+
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2495
|
+
);
|
|
2496
|
+
const cards = [];
|
|
2497
|
+
for (const cardId of practiceCardIds) {
|
|
2498
|
+
emittedIds.add(cardId);
|
|
2499
|
+
cards.push({
|
|
2500
|
+
cardId,
|
|
2501
|
+
courseId,
|
|
2502
|
+
score: BASE_PRACTICE_SCORE,
|
|
2503
|
+
provenance: [
|
|
2504
|
+
{
|
|
2505
|
+
strategy: "prescribed",
|
|
2506
|
+
strategyName: this.strategyName || this.name,
|
|
2507
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2508
|
+
action: "generated",
|
|
2509
|
+
score: BASE_PRACTICE_SCORE,
|
|
2510
|
+
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2511
|
+
}
|
|
2512
|
+
]
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
return cards;
|
|
2516
|
+
}
|
|
2517
|
+
/**
|
|
2518
|
+
* True for a skill that was *gated and is now reached*: it has at least one
|
|
2519
|
+
* declared hierarchy prerequisite set, and every set is fully satisfied by the
|
|
2520
|
+
* learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
|
|
2521
|
+
* — an ungated tag was never "introduced" in the curricular sense, so it isn't
|
|
2522
|
+
* a post-intro drill target (e.g. whole-word spelling tags that share the
|
|
2523
|
+
* `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
|
|
2524
|
+
* ELO retrieval. This is the precise population the retrieval gap strands:
|
|
2525
|
+
* just-unlocked, low-ELO skills.
|
|
2526
|
+
*/
|
|
2527
|
+
isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
|
|
2528
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
2529
|
+
if (prereqSets.length === 0) return false;
|
|
2530
|
+
return prereqSets.every(
|
|
2531
|
+
(prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
2532
|
+
);
|
|
2533
|
+
}
|
|
2335
2534
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2336
2535
|
if (supportTags.length === 0) {
|
|
2337
2536
|
return [];
|
|
@@ -4126,7 +4325,7 @@ function logResultCards(cards) {
|
|
|
4126
4325
|
for (let i = 0; i < cards.length; i++) {
|
|
4127
4326
|
const c = cards[i];
|
|
4128
4327
|
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
4129
|
-
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
4328
|
+
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) => {
|
|
4130
4329
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
4131
4330
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
4132
4331
|
}).join(" | ");
|
|
@@ -4157,6 +4356,7 @@ var init_Pipeline = __esm({
|
|
|
4157
4356
|
init_logger();
|
|
4158
4357
|
init_orchestration();
|
|
4159
4358
|
init_PipelineDebugger();
|
|
4359
|
+
init_diversityRerank();
|
|
4160
4360
|
VERBOSE_RESULTS = true;
|
|
4161
4361
|
Pipeline = class extends ContentNavigator {
|
|
4162
4362
|
generator;
|
|
@@ -4330,6 +4530,7 @@ var init_Pipeline = __esm({
|
|
|
4330
4530
|
this._ephemeralHints = null;
|
|
4331
4531
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
4332
4532
|
}
|
|
4533
|
+
cards = diversityRerank(cards);
|
|
4333
4534
|
cards.sort((a, b) => b.score - a.score);
|
|
4334
4535
|
const tFilter = performance.now();
|
|
4335
4536
|
const result = cards.slice(0, limit);
|
|
@@ -4633,6 +4834,68 @@ var init_Pipeline = __esm({
|
|
|
4633
4834
|
// ---------------------------------------------------------------------------
|
|
4634
4835
|
// Card-space diagnostic
|
|
4635
4836
|
// ---------------------------------------------------------------------------
|
|
4837
|
+
/**
|
|
4838
|
+
* Commit-free forecast: score the user's full card space through the filter
|
|
4839
|
+
* chain and return the cards that are currently *reachable* (score >=
|
|
4840
|
+
* threshold), optionally nudged by caller-supplied hints and/or restricted
|
|
4841
|
+
* to cards the user hasn't seen yet.
|
|
4842
|
+
*
|
|
4843
|
+
* This is a GENERIC primitive — it returns scored, tag-hydrated cards and
|
|
4844
|
+
* stops there. It has no knowledge of any particular tag convention; callers
|
|
4845
|
+
* decide what the surviving cards mean (e.g. filter to their own "intro"
|
|
4846
|
+
* tag family). Nothing is written and no session is started.
|
|
4847
|
+
*
|
|
4848
|
+
* The optional `hints` are the "out-of-band kick": they run through the same
|
|
4849
|
+
* {@link applyHints} path a live replan uses, so the two semantics carry over —
|
|
4850
|
+
* - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
|
|
4851
|
+
* stays out), and
|
|
4852
|
+
* - `requireTags`/`requireCards` inject from the full pre-filter pool,
|
|
4853
|
+
* *bypassing* gating (use when you want a card regardless of reachability).
|
|
4854
|
+
* Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
|
|
4855
|
+
* user has already seen — pass `unseenOnly: false` if that matters.
|
|
4856
|
+
*
|
|
4857
|
+
* Cost note: like {@link diagnoseCardSpace}, this scans every card through the
|
|
4858
|
+
* filters, so it's heavier than a normal replan. Intended for one-shot
|
|
4859
|
+
* out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
|
|
4860
|
+
*
|
|
4861
|
+
* @param opts.hints Optional ephemeral hints to apply after the filter chain.
|
|
4862
|
+
* @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
|
|
4863
|
+
* @param opts.threshold Min score to count as reachable (default 0.10).
|
|
4864
|
+
* @param opts.limit Optional cap on results (already sorted desc).
|
|
4865
|
+
*/
|
|
4866
|
+
async forecast(opts) {
|
|
4867
|
+
const threshold = opts?.threshold ?? 0.1;
|
|
4868
|
+
const unseenOnly = opts?.unseenOnly ?? true;
|
|
4869
|
+
const courseId = this.course.getCourseID();
|
|
4870
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
4871
|
+
let cards = allCardIds.map((cardId) => ({
|
|
4872
|
+
cardId,
|
|
4873
|
+
courseId,
|
|
4874
|
+
score: 1,
|
|
4875
|
+
provenance: []
|
|
4876
|
+
}));
|
|
4877
|
+
cards = await this.hydrateTags(cards);
|
|
4878
|
+
const fullPool = cards.slice();
|
|
4879
|
+
const context = await this.buildContext();
|
|
4880
|
+
for (const filter of this.filters) {
|
|
4881
|
+
cards = await filter.transform(cards, context);
|
|
4882
|
+
}
|
|
4883
|
+
if (opts?.hints) {
|
|
4884
|
+
cards = this.applyHints(cards, opts.hints, fullPool);
|
|
4885
|
+
}
|
|
4886
|
+
cards = cards.filter((c) => c.score >= threshold);
|
|
4887
|
+
if (unseenOnly) {
|
|
4888
|
+
let encountered;
|
|
4889
|
+
try {
|
|
4890
|
+
encountered = new Set(await this.user.getSeenCards(courseId));
|
|
4891
|
+
} catch {
|
|
4892
|
+
encountered = /* @__PURE__ */ new Set();
|
|
4893
|
+
}
|
|
4894
|
+
cards = cards.filter((c) => !encountered.has(c.cardId));
|
|
4895
|
+
}
|
|
4896
|
+
cards.sort((a, b) => b.score - a.score);
|
|
4897
|
+
return opts?.limit ? cards.slice(0, opts.limit) : cards;
|
|
4898
|
+
}
|
|
4636
4899
|
/**
|
|
4637
4900
|
* Scan every card in the course through the filter chain and report
|
|
4638
4901
|
* how many are "well indicated" (score >= threshold) for the current user.
|
|
@@ -4898,6 +5161,7 @@ var init_3 = __esm({
|
|
|
4898
5161
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4899
5162
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4900
5163
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5164
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4901
5165
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
4902
5166
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
4903
5167
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -4923,9 +5187,13 @@ var init_3 = __esm({
|
|
|
4923
5187
|
var navigators_exports = {};
|
|
4924
5188
|
__export(navigators_exports, {
|
|
4925
5189
|
ContentNavigator: () => ContentNavigator,
|
|
5190
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
5191
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
4926
5192
|
NavigatorRole: () => NavigatorRole,
|
|
4927
5193
|
NavigatorRoles: () => NavigatorRoles,
|
|
4928
5194
|
Navigators: () => Navigators,
|
|
5195
|
+
diversityRerank: () => diversityRerank,
|
|
5196
|
+
getActivePipeline: () => getActivePipeline,
|
|
4929
5197
|
getCardOrigin: () => getCardOrigin,
|
|
4930
5198
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
4931
5199
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -5009,6 +5277,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
5009
5277
|
var init_navigators = __esm({
|
|
5010
5278
|
"src/core/navigators/index.ts"() {
|
|
5011
5279
|
"use strict";
|
|
5280
|
+
init_diversityRerank();
|
|
5012
5281
|
init_PipelineDebugger();
|
|
5013
5282
|
init_logger();
|
|
5014
5283
|
init_();
|
|
@@ -8032,6 +8301,8 @@ var init_core = __esm({
|
|
|
8032
8301
|
init_core();
|
|
8033
8302
|
export {
|
|
8034
8303
|
ContentNavigator,
|
|
8304
|
+
DIVERSITY_FLOOR,
|
|
8305
|
+
DIVERSITY_STRENGTH,
|
|
8035
8306
|
DocType,
|
|
8036
8307
|
DocTypePrefixes,
|
|
8037
8308
|
GuestUsername,
|
|
@@ -8048,7 +8319,9 @@ export {
|
|
|
8048
8319
|
computeSpread,
|
|
8049
8320
|
computeStrategyGradient,
|
|
8050
8321
|
createOrchestrationContext,
|
|
8322
|
+
diversityRerank,
|
|
8051
8323
|
docIsDeleted,
|
|
8324
|
+
getActivePipeline,
|
|
8052
8325
|
getCardHistoryID,
|
|
8053
8326
|
getCardOrigin,
|
|
8054
8327
|
getDefaultLearnableWeight,
|