@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/impl/couch/index.js
CHANGED
|
@@ -643,12 +643,102 @@ var init_courseLookupDB = __esm({
|
|
|
643
643
|
}
|
|
644
644
|
});
|
|
645
645
|
|
|
646
|
+
// src/core/navigators/diversityRerank.ts
|
|
647
|
+
var diversityRerank_exports = {};
|
|
648
|
+
__export(diversityRerank_exports, {
|
|
649
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
650
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
651
|
+
diversityRerank: () => diversityRerank
|
|
652
|
+
});
|
|
653
|
+
function diversityRerank(cards, opts = {}) {
|
|
654
|
+
const strength = opts.strength ?? DIVERSITY_STRENGTH;
|
|
655
|
+
const floor = opts.floor ?? DIVERSITY_FLOOR;
|
|
656
|
+
const n = cards.length;
|
|
657
|
+
if (n <= 1) return cards;
|
|
658
|
+
const df = /* @__PURE__ */ new Map();
|
|
659
|
+
for (const card of cards) {
|
|
660
|
+
for (const tag of card.tags ?? []) {
|
|
661
|
+
df.set(tag, (df.get(tag) ?? 0) + 1);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const idf = /* @__PURE__ */ new Map();
|
|
665
|
+
for (const [tag, freq] of df) {
|
|
666
|
+
idf.set(tag, Math.log(n / freq));
|
|
667
|
+
}
|
|
668
|
+
const remaining = [...cards];
|
|
669
|
+
const emittedCount = /* @__PURE__ */ new Map();
|
|
670
|
+
const out = [];
|
|
671
|
+
const repetitionLoad = (card) => {
|
|
672
|
+
let load = 0;
|
|
673
|
+
for (const tag of card.tags ?? []) {
|
|
674
|
+
const seen = emittedCount.get(tag);
|
|
675
|
+
if (seen) load += (idf.get(tag) ?? 0) * seen;
|
|
676
|
+
}
|
|
677
|
+
return load;
|
|
678
|
+
};
|
|
679
|
+
while (remaining.length > 0) {
|
|
680
|
+
let bestIdx = 0;
|
|
681
|
+
let bestValue = -Infinity;
|
|
682
|
+
let bestPenalty = 1;
|
|
683
|
+
let bestLoad = 0;
|
|
684
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
685
|
+
const card = remaining[i];
|
|
686
|
+
const load = repetitionLoad(card);
|
|
687
|
+
const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
|
|
688
|
+
const value = card.score * penalty;
|
|
689
|
+
if (value > bestValue) {
|
|
690
|
+
bestValue = value;
|
|
691
|
+
bestIdx = i;
|
|
692
|
+
bestPenalty = penalty;
|
|
693
|
+
bestLoad = load;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
const [picked] = remaining.splice(bestIdx, 1);
|
|
697
|
+
if (Number.isFinite(picked.score) && bestPenalty < 1) {
|
|
698
|
+
const newScore = picked.score * bestPenalty;
|
|
699
|
+
out.push({
|
|
700
|
+
...picked,
|
|
701
|
+
score: newScore,
|
|
702
|
+
provenance: [
|
|
703
|
+
...picked.provenance,
|
|
704
|
+
{
|
|
705
|
+
strategy: STRATEGY,
|
|
706
|
+
strategyId: STRATEGY_ID,
|
|
707
|
+
strategyName: STRATEGY_NAME,
|
|
708
|
+
action: "penalized",
|
|
709
|
+
score: newScore,
|
|
710
|
+
reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
|
|
711
|
+
}
|
|
712
|
+
]
|
|
713
|
+
});
|
|
714
|
+
} else {
|
|
715
|
+
out.push(picked);
|
|
716
|
+
}
|
|
717
|
+
for (const tag of picked.tags ?? []) {
|
|
718
|
+
emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
return out;
|
|
722
|
+
}
|
|
723
|
+
var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
|
|
724
|
+
var init_diversityRerank = __esm({
|
|
725
|
+
"src/core/navigators/diversityRerank.ts"() {
|
|
726
|
+
"use strict";
|
|
727
|
+
DIVERSITY_STRENGTH = 0.6;
|
|
728
|
+
DIVERSITY_FLOOR = 0.3;
|
|
729
|
+
STRATEGY = "diversityRerank";
|
|
730
|
+
STRATEGY_ID = "DIVERSITY_RERANK";
|
|
731
|
+
STRATEGY_NAME = "Diversity Re-rank";
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
|
|
646
735
|
// src/core/navigators/PipelineDebugger.ts
|
|
647
736
|
var PipelineDebugger_exports = {};
|
|
648
737
|
__export(PipelineDebugger_exports, {
|
|
649
738
|
buildRunReport: () => buildRunReport,
|
|
650
739
|
captureRun: () => captureRun,
|
|
651
740
|
clearRunHistory: () => clearRunHistory,
|
|
741
|
+
getActivePipeline: () => getActivePipeline,
|
|
652
742
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
653
743
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
654
744
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -656,6 +746,9 @@ __export(PipelineDebugger_exports, {
|
|
|
656
746
|
function registerPipelineForDebug(pipeline) {
|
|
657
747
|
_activePipeline = pipeline;
|
|
658
748
|
}
|
|
749
|
+
function getActivePipeline() {
|
|
750
|
+
return _activePipeline;
|
|
751
|
+
}
|
|
659
752
|
function clearRunHistory() {
|
|
660
753
|
runHistory.length = 0;
|
|
661
754
|
}
|
|
@@ -1838,7 +1931,7 @@ function shuffleInPlace(arr) {
|
|
|
1838
1931
|
function pickTopByScore(cards, limit) {
|
|
1839
1932
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1840
1933
|
}
|
|
1841
|
-
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;
|
|
1934
|
+
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;
|
|
1842
1935
|
var init_prescribed = __esm({
|
|
1843
1936
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1844
1937
|
"use strict";
|
|
@@ -1849,9 +1942,12 @@ var init_prescribed = __esm({
|
|
|
1849
1942
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1850
1943
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1851
1944
|
DEFAULT_MIN_COUNT = 3;
|
|
1945
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
1946
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
1852
1947
|
BASE_TARGET_SCORE = 1;
|
|
1853
1948
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1854
1949
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
1950
|
+
BASE_PRACTICE_SCORE = 1;
|
|
1855
1951
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1856
1952
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1857
1953
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -1959,7 +2055,18 @@ var init_prescribed = __esm({
|
|
|
1959
2055
|
courseId,
|
|
1960
2056
|
emittedIds
|
|
1961
2057
|
);
|
|
1962
|
-
|
|
2058
|
+
const practiceCards = this.buildPracticeCards({
|
|
2059
|
+
group,
|
|
2060
|
+
courseId,
|
|
2061
|
+
emittedIds,
|
|
2062
|
+
cardsByTag,
|
|
2063
|
+
hierarchyConfigs,
|
|
2064
|
+
userTagElo,
|
|
2065
|
+
userGlobalElo,
|
|
2066
|
+
activeIds,
|
|
2067
|
+
seenIds
|
|
2068
|
+
});
|
|
2069
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
1963
2070
|
}
|
|
1964
2071
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
1965
2072
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -1987,6 +2094,7 @@ var init_prescribed = __esm({
|
|
|
1987
2094
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
1988
2095
|
for (const card of finalCards) {
|
|
1989
2096
|
const prov = card.provenance[0];
|
|
2097
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
1990
2098
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
1991
2099
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
1992
2100
|
if (!groupId) continue;
|
|
@@ -2056,7 +2164,12 @@ var init_prescribed = __esm({
|
|
|
2056
2164
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
2057
2165
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
2058
2166
|
},
|
|
2059
|
-
retireOnEncounter: raw.retireOnEncounter !== false
|
|
2167
|
+
retireOnEncounter: raw.retireOnEncounter !== false,
|
|
2168
|
+
practiceTagPatterns: dedupe(
|
|
2169
|
+
Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
|
|
2170
|
+
),
|
|
2171
|
+
practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
|
|
2172
|
+
maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
|
|
2060
2173
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
2061
2174
|
return { groups };
|
|
2062
2175
|
} catch {
|
|
@@ -2279,6 +2392,92 @@ var init_prescribed = __esm({
|
|
|
2279
2392
|
}
|
|
2280
2393
|
return cards;
|
|
2281
2394
|
}
|
|
2395
|
+
/**
|
|
2396
|
+
* Emit drill cards for *unlocked-but-under-practiced* skills.
|
|
2397
|
+
*
|
|
2398
|
+
* For each course tag matching the group's `practiceTagPatterns` that is both
|
|
2399
|
+
* unlocked (all hierarchy prerequisites met — i.e. the learner has been
|
|
2400
|
+
* introduced to it) and under-practiced (per-tag attempt count below
|
|
2401
|
+
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2402
|
+
* into the candidate pool. It exists because global-ELO retrieval
|
|
2403
|
+
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2404
|
+
* freshly-introduced skill — putting them in the pool here lets the pipeline's
|
|
2405
|
+
* scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
|
|
2406
|
+
* this method's job; it only guarantees presence.
|
|
2407
|
+
*
|
|
2408
|
+
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2409
|
+
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
2410
|
+
*/
|
|
2411
|
+
buildPracticeCards(args) {
|
|
2412
|
+
const {
|
|
2413
|
+
group,
|
|
2414
|
+
courseId,
|
|
2415
|
+
emittedIds,
|
|
2416
|
+
cardsByTag,
|
|
2417
|
+
hierarchyConfigs,
|
|
2418
|
+
userTagElo,
|
|
2419
|
+
userGlobalElo,
|
|
2420
|
+
activeIds,
|
|
2421
|
+
seenIds
|
|
2422
|
+
} = args;
|
|
2423
|
+
const patterns = group.practiceTagPatterns ?? [];
|
|
2424
|
+
if (patterns.length === 0) return [];
|
|
2425
|
+
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2426
|
+
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2427
|
+
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2428
|
+
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2429
|
+
);
|
|
2430
|
+
if (practiceTags.length === 0) return [];
|
|
2431
|
+
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2432
|
+
supportTags: practiceTags,
|
|
2433
|
+
cardsByTag,
|
|
2434
|
+
activeIds,
|
|
2435
|
+
seenIds,
|
|
2436
|
+
excludedIds: emittedIds,
|
|
2437
|
+
limit: maxPractice
|
|
2438
|
+
});
|
|
2439
|
+
if (practiceCardIds.length === 0) return [];
|
|
2440
|
+
logger.info(
|
|
2441
|
+
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2442
|
+
);
|
|
2443
|
+
const cards = [];
|
|
2444
|
+
for (const cardId of practiceCardIds) {
|
|
2445
|
+
emittedIds.add(cardId);
|
|
2446
|
+
cards.push({
|
|
2447
|
+
cardId,
|
|
2448
|
+
courseId,
|
|
2449
|
+
score: BASE_PRACTICE_SCORE,
|
|
2450
|
+
provenance: [
|
|
2451
|
+
{
|
|
2452
|
+
strategy: "prescribed",
|
|
2453
|
+
strategyName: this.strategyName || this.name,
|
|
2454
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2455
|
+
action: "generated",
|
|
2456
|
+
score: BASE_PRACTICE_SCORE,
|
|
2457
|
+
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2458
|
+
}
|
|
2459
|
+
]
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
return cards;
|
|
2463
|
+
}
|
|
2464
|
+
/**
|
|
2465
|
+
* True for a skill that was *gated and is now reached*: it has at least one
|
|
2466
|
+
* declared hierarchy prerequisite set, and every set is fully satisfied by the
|
|
2467
|
+
* learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
|
|
2468
|
+
* — an ungated tag was never "introduced" in the curricular sense, so it isn't
|
|
2469
|
+
* a post-intro drill target (e.g. whole-word spelling tags that share the
|
|
2470
|
+
* `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
|
|
2471
|
+
* ELO retrieval. This is the precise population the retrieval gap strands:
|
|
2472
|
+
* just-unlocked, low-ELO skills.
|
|
2473
|
+
*/
|
|
2474
|
+
isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
|
|
2475
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
2476
|
+
if (prereqSets.length === 0) return false;
|
|
2477
|
+
return prereqSets.every(
|
|
2478
|
+
(prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
2479
|
+
);
|
|
2480
|
+
}
|
|
2282
2481
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2283
2482
|
if (supportTags.length === 0) {
|
|
2284
2483
|
return [];
|
|
@@ -3826,7 +4025,7 @@ function logResultCards(cards) {
|
|
|
3826
4025
|
for (let i = 0; i < cards.length; i++) {
|
|
3827
4026
|
const c = cards[i];
|
|
3828
4027
|
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
3829
|
-
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
4028
|
+
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) => {
|
|
3830
4029
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
3831
4030
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
3832
4031
|
}).join(" | ");
|
|
@@ -3858,6 +4057,7 @@ var init_Pipeline = __esm({
|
|
|
3858
4057
|
init_logger();
|
|
3859
4058
|
init_orchestration();
|
|
3860
4059
|
init_PipelineDebugger();
|
|
4060
|
+
init_diversityRerank();
|
|
3861
4061
|
VERBOSE_RESULTS = true;
|
|
3862
4062
|
Pipeline = class extends ContentNavigator {
|
|
3863
4063
|
generator;
|
|
@@ -4031,6 +4231,7 @@ var init_Pipeline = __esm({
|
|
|
4031
4231
|
this._ephemeralHints = null;
|
|
4032
4232
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
4033
4233
|
}
|
|
4234
|
+
cards = diversityRerank(cards);
|
|
4034
4235
|
cards.sort((a, b) => b.score - a.score);
|
|
4035
4236
|
const tFilter = performance.now();
|
|
4036
4237
|
const result = cards.slice(0, limit);
|
|
@@ -4334,6 +4535,68 @@ var init_Pipeline = __esm({
|
|
|
4334
4535
|
// ---------------------------------------------------------------------------
|
|
4335
4536
|
// Card-space diagnostic
|
|
4336
4537
|
// ---------------------------------------------------------------------------
|
|
4538
|
+
/**
|
|
4539
|
+
* Commit-free forecast: score the user's full card space through the filter
|
|
4540
|
+
* chain and return the cards that are currently *reachable* (score >=
|
|
4541
|
+
* threshold), optionally nudged by caller-supplied hints and/or restricted
|
|
4542
|
+
* to cards the user hasn't seen yet.
|
|
4543
|
+
*
|
|
4544
|
+
* This is a GENERIC primitive — it returns scored, tag-hydrated cards and
|
|
4545
|
+
* stops there. It has no knowledge of any particular tag convention; callers
|
|
4546
|
+
* decide what the surviving cards mean (e.g. filter to their own "intro"
|
|
4547
|
+
* tag family). Nothing is written and no session is started.
|
|
4548
|
+
*
|
|
4549
|
+
* The optional `hints` are the "out-of-band kick": they run through the same
|
|
4550
|
+
* {@link applyHints} path a live replan uses, so the two semantics carry over —
|
|
4551
|
+
* - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
|
|
4552
|
+
* stays out), and
|
|
4553
|
+
* - `requireTags`/`requireCards` inject from the full pre-filter pool,
|
|
4554
|
+
* *bypassing* gating (use when you want a card regardless of reachability).
|
|
4555
|
+
* Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
|
|
4556
|
+
* user has already seen — pass `unseenOnly: false` if that matters.
|
|
4557
|
+
*
|
|
4558
|
+
* Cost note: like {@link diagnoseCardSpace}, this scans every card through the
|
|
4559
|
+
* filters, so it's heavier than a normal replan. Intended for one-shot
|
|
4560
|
+
* out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
|
|
4561
|
+
*
|
|
4562
|
+
* @param opts.hints Optional ephemeral hints to apply after the filter chain.
|
|
4563
|
+
* @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
|
|
4564
|
+
* @param opts.threshold Min score to count as reachable (default 0.10).
|
|
4565
|
+
* @param opts.limit Optional cap on results (already sorted desc).
|
|
4566
|
+
*/
|
|
4567
|
+
async forecast(opts) {
|
|
4568
|
+
const threshold = opts?.threshold ?? 0.1;
|
|
4569
|
+
const unseenOnly = opts?.unseenOnly ?? true;
|
|
4570
|
+
const courseId = this.course.getCourseID();
|
|
4571
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
4572
|
+
let cards = allCardIds.map((cardId) => ({
|
|
4573
|
+
cardId,
|
|
4574
|
+
courseId,
|
|
4575
|
+
score: 1,
|
|
4576
|
+
provenance: []
|
|
4577
|
+
}));
|
|
4578
|
+
cards = await this.hydrateTags(cards);
|
|
4579
|
+
const fullPool = cards.slice();
|
|
4580
|
+
const context = await this.buildContext();
|
|
4581
|
+
for (const filter of this.filters) {
|
|
4582
|
+
cards = await filter.transform(cards, context);
|
|
4583
|
+
}
|
|
4584
|
+
if (opts?.hints) {
|
|
4585
|
+
cards = this.applyHints(cards, opts.hints, fullPool);
|
|
4586
|
+
}
|
|
4587
|
+
cards = cards.filter((c) => c.score >= threshold);
|
|
4588
|
+
if (unseenOnly) {
|
|
4589
|
+
let encountered;
|
|
4590
|
+
try {
|
|
4591
|
+
encountered = new Set(await this.user.getSeenCards(courseId));
|
|
4592
|
+
} catch {
|
|
4593
|
+
encountered = /* @__PURE__ */ new Set();
|
|
4594
|
+
}
|
|
4595
|
+
cards = cards.filter((c) => !encountered.has(c.cardId));
|
|
4596
|
+
}
|
|
4597
|
+
cards.sort((a, b) => b.score - a.score);
|
|
4598
|
+
return opts?.limit ? cards.slice(0, opts.limit) : cards;
|
|
4599
|
+
}
|
|
4337
4600
|
/**
|
|
4338
4601
|
* Scan every card in the course through the filter chain and report
|
|
4339
4602
|
* how many are "well indicated" (score >= threshold) for the current user.
|
|
@@ -4599,6 +4862,7 @@ var init_3 = __esm({
|
|
|
4599
4862
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4600
4863
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4601
4864
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
4865
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4602
4866
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
4603
4867
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
4604
4868
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -4624,9 +4888,13 @@ var init_3 = __esm({
|
|
|
4624
4888
|
var navigators_exports = {};
|
|
4625
4889
|
__export(navigators_exports, {
|
|
4626
4890
|
ContentNavigator: () => ContentNavigator,
|
|
4891
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
4892
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
4627
4893
|
NavigatorRole: () => NavigatorRole,
|
|
4628
4894
|
NavigatorRoles: () => NavigatorRoles,
|
|
4629
4895
|
Navigators: () => Navigators,
|
|
4896
|
+
diversityRerank: () => diversityRerank,
|
|
4897
|
+
getActivePipeline: () => getActivePipeline,
|
|
4630
4898
|
getCardOrigin: () => getCardOrigin,
|
|
4631
4899
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
4632
4900
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -4710,6 +4978,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
4710
4978
|
var init_navigators = __esm({
|
|
4711
4979
|
"src/core/navigators/index.ts"() {
|
|
4712
4980
|
"use strict";
|
|
4981
|
+
init_diversityRerank();
|
|
4713
4982
|
init_PipelineDebugger();
|
|
4714
4983
|
init_logger();
|
|
4715
4984
|
init_();
|