@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/index.mjs
CHANGED
|
@@ -849,12 +849,102 @@ var init_courseLookupDB = __esm({
|
|
|
849
849
|
}
|
|
850
850
|
});
|
|
851
851
|
|
|
852
|
+
// src/core/navigators/diversityRerank.ts
|
|
853
|
+
var diversityRerank_exports = {};
|
|
854
|
+
__export(diversityRerank_exports, {
|
|
855
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
856
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
857
|
+
diversityRerank: () => diversityRerank
|
|
858
|
+
});
|
|
859
|
+
function diversityRerank(cards, opts = {}) {
|
|
860
|
+
const strength = opts.strength ?? DIVERSITY_STRENGTH;
|
|
861
|
+
const floor = opts.floor ?? DIVERSITY_FLOOR;
|
|
862
|
+
const n = cards.length;
|
|
863
|
+
if (n <= 1) return cards;
|
|
864
|
+
const df = /* @__PURE__ */ new Map();
|
|
865
|
+
for (const card of cards) {
|
|
866
|
+
for (const tag of card.tags ?? []) {
|
|
867
|
+
df.set(tag, (df.get(tag) ?? 0) + 1);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const idf = /* @__PURE__ */ new Map();
|
|
871
|
+
for (const [tag, freq] of df) {
|
|
872
|
+
idf.set(tag, Math.log(n / freq));
|
|
873
|
+
}
|
|
874
|
+
const remaining = [...cards];
|
|
875
|
+
const emittedCount = /* @__PURE__ */ new Map();
|
|
876
|
+
const out = [];
|
|
877
|
+
const repetitionLoad = (card) => {
|
|
878
|
+
let load = 0;
|
|
879
|
+
for (const tag of card.tags ?? []) {
|
|
880
|
+
const seen = emittedCount.get(tag);
|
|
881
|
+
if (seen) load += (idf.get(tag) ?? 0) * seen;
|
|
882
|
+
}
|
|
883
|
+
return load;
|
|
884
|
+
};
|
|
885
|
+
while (remaining.length > 0) {
|
|
886
|
+
let bestIdx = 0;
|
|
887
|
+
let bestValue = -Infinity;
|
|
888
|
+
let bestPenalty = 1;
|
|
889
|
+
let bestLoad = 0;
|
|
890
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
891
|
+
const card = remaining[i];
|
|
892
|
+
const load = repetitionLoad(card);
|
|
893
|
+
const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
|
|
894
|
+
const value = card.score * penalty;
|
|
895
|
+
if (value > bestValue) {
|
|
896
|
+
bestValue = value;
|
|
897
|
+
bestIdx = i;
|
|
898
|
+
bestPenalty = penalty;
|
|
899
|
+
bestLoad = load;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const [picked] = remaining.splice(bestIdx, 1);
|
|
903
|
+
if (Number.isFinite(picked.score) && bestPenalty < 1) {
|
|
904
|
+
const newScore = picked.score * bestPenalty;
|
|
905
|
+
out.push({
|
|
906
|
+
...picked,
|
|
907
|
+
score: newScore,
|
|
908
|
+
provenance: [
|
|
909
|
+
...picked.provenance,
|
|
910
|
+
{
|
|
911
|
+
strategy: STRATEGY,
|
|
912
|
+
strategyId: STRATEGY_ID,
|
|
913
|
+
strategyName: STRATEGY_NAME,
|
|
914
|
+
action: "penalized",
|
|
915
|
+
score: newScore,
|
|
916
|
+
reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
|
|
917
|
+
}
|
|
918
|
+
]
|
|
919
|
+
});
|
|
920
|
+
} else {
|
|
921
|
+
out.push(picked);
|
|
922
|
+
}
|
|
923
|
+
for (const tag of picked.tags ?? []) {
|
|
924
|
+
emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return out;
|
|
928
|
+
}
|
|
929
|
+
var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
|
|
930
|
+
var init_diversityRerank = __esm({
|
|
931
|
+
"src/core/navigators/diversityRerank.ts"() {
|
|
932
|
+
"use strict";
|
|
933
|
+
DIVERSITY_STRENGTH = 0.6;
|
|
934
|
+
DIVERSITY_FLOOR = 0.3;
|
|
935
|
+
STRATEGY = "diversityRerank";
|
|
936
|
+
STRATEGY_ID = "DIVERSITY_RERANK";
|
|
937
|
+
STRATEGY_NAME = "Diversity Re-rank";
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
852
941
|
// src/core/navigators/PipelineDebugger.ts
|
|
853
942
|
var PipelineDebugger_exports = {};
|
|
854
943
|
__export(PipelineDebugger_exports, {
|
|
855
944
|
buildRunReport: () => buildRunReport,
|
|
856
945
|
captureRun: () => captureRun,
|
|
857
946
|
clearRunHistory: () => clearRunHistory,
|
|
947
|
+
getActivePipeline: () => getActivePipeline,
|
|
858
948
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
859
949
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
860
950
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -862,6 +952,9 @@ __export(PipelineDebugger_exports, {
|
|
|
862
952
|
function registerPipelineForDebug(pipeline) {
|
|
863
953
|
_activePipeline = pipeline;
|
|
864
954
|
}
|
|
955
|
+
function getActivePipeline() {
|
|
956
|
+
return _activePipeline;
|
|
957
|
+
}
|
|
865
958
|
function clearRunHistory() {
|
|
866
959
|
runHistory.length = 0;
|
|
867
960
|
}
|
|
@@ -2044,7 +2137,7 @@ function shuffleInPlace(arr) {
|
|
|
2044
2137
|
function pickTopByScore(cards, limit) {
|
|
2045
2138
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
2046
2139
|
}
|
|
2047
|
-
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;
|
|
2140
|
+
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;
|
|
2048
2141
|
var init_prescribed = __esm({
|
|
2049
2142
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
2050
2143
|
"use strict";
|
|
@@ -2055,9 +2148,12 @@ var init_prescribed = __esm({
|
|
|
2055
2148
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
2056
2149
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
2057
2150
|
DEFAULT_MIN_COUNT = 3;
|
|
2151
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
2152
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
2058
2153
|
BASE_TARGET_SCORE = 1;
|
|
2059
2154
|
BASE_SUPPORT_SCORE = 0.8;
|
|
2060
2155
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
2156
|
+
BASE_PRACTICE_SCORE = 1;
|
|
2061
2157
|
MAX_TARGET_MULTIPLIER = 8;
|
|
2062
2158
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
2063
2159
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -2165,7 +2261,18 @@ var init_prescribed = __esm({
|
|
|
2165
2261
|
courseId,
|
|
2166
2262
|
emittedIds
|
|
2167
2263
|
);
|
|
2168
|
-
|
|
2264
|
+
const practiceCards = this.buildPracticeCards({
|
|
2265
|
+
group,
|
|
2266
|
+
courseId,
|
|
2267
|
+
emittedIds,
|
|
2268
|
+
cardsByTag,
|
|
2269
|
+
hierarchyConfigs,
|
|
2270
|
+
userTagElo,
|
|
2271
|
+
userGlobalElo,
|
|
2272
|
+
activeIds,
|
|
2273
|
+
seenIds
|
|
2274
|
+
});
|
|
2275
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
2169
2276
|
}
|
|
2170
2277
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2171
2278
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -2193,6 +2300,7 @@ var init_prescribed = __esm({
|
|
|
2193
2300
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
2194
2301
|
for (const card of finalCards) {
|
|
2195
2302
|
const prov = card.provenance[0];
|
|
2303
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
2196
2304
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
2197
2305
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
2198
2306
|
if (!groupId) continue;
|
|
@@ -2262,7 +2370,12 @@ var init_prescribed = __esm({
|
|
|
2262
2370
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
2263
2371
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
2264
2372
|
},
|
|
2265
|
-
retireOnEncounter: raw.retireOnEncounter !== false
|
|
2373
|
+
retireOnEncounter: raw.retireOnEncounter !== false,
|
|
2374
|
+
practiceTagPatterns: dedupe(
|
|
2375
|
+
Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
|
|
2376
|
+
),
|
|
2377
|
+
practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
|
|
2378
|
+
maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
|
|
2266
2379
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
2267
2380
|
return { groups };
|
|
2268
2381
|
} catch {
|
|
@@ -2485,6 +2598,92 @@ var init_prescribed = __esm({
|
|
|
2485
2598
|
}
|
|
2486
2599
|
return cards;
|
|
2487
2600
|
}
|
|
2601
|
+
/**
|
|
2602
|
+
* Emit drill cards for *unlocked-but-under-practiced* skills.
|
|
2603
|
+
*
|
|
2604
|
+
* For each course tag matching the group's `practiceTagPatterns` that is both
|
|
2605
|
+
* unlocked (all hierarchy prerequisites met — i.e. the learner has been
|
|
2606
|
+
* introduced to it) and under-practiced (per-tag attempt count below
|
|
2607
|
+
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2608
|
+
* into the candidate pool. It exists because global-ELO retrieval
|
|
2609
|
+
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2610
|
+
* freshly-introduced skill — putting them in the pool here lets the pipeline's
|
|
2611
|
+
* scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
|
|
2612
|
+
* this method's job; it only guarantees presence.
|
|
2613
|
+
*
|
|
2614
|
+
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2615
|
+
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
2616
|
+
*/
|
|
2617
|
+
buildPracticeCards(args) {
|
|
2618
|
+
const {
|
|
2619
|
+
group,
|
|
2620
|
+
courseId,
|
|
2621
|
+
emittedIds,
|
|
2622
|
+
cardsByTag,
|
|
2623
|
+
hierarchyConfigs,
|
|
2624
|
+
userTagElo,
|
|
2625
|
+
userGlobalElo,
|
|
2626
|
+
activeIds,
|
|
2627
|
+
seenIds
|
|
2628
|
+
} = args;
|
|
2629
|
+
const patterns = group.practiceTagPatterns ?? [];
|
|
2630
|
+
if (patterns.length === 0) return [];
|
|
2631
|
+
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2632
|
+
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2633
|
+
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2634
|
+
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2635
|
+
);
|
|
2636
|
+
if (practiceTags.length === 0) return [];
|
|
2637
|
+
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2638
|
+
supportTags: practiceTags,
|
|
2639
|
+
cardsByTag,
|
|
2640
|
+
activeIds,
|
|
2641
|
+
seenIds,
|
|
2642
|
+
excludedIds: emittedIds,
|
|
2643
|
+
limit: maxPractice
|
|
2644
|
+
});
|
|
2645
|
+
if (practiceCardIds.length === 0) return [];
|
|
2646
|
+
logger.info(
|
|
2647
|
+
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2648
|
+
);
|
|
2649
|
+
const cards = [];
|
|
2650
|
+
for (const cardId of practiceCardIds) {
|
|
2651
|
+
emittedIds.add(cardId);
|
|
2652
|
+
cards.push({
|
|
2653
|
+
cardId,
|
|
2654
|
+
courseId,
|
|
2655
|
+
score: BASE_PRACTICE_SCORE,
|
|
2656
|
+
provenance: [
|
|
2657
|
+
{
|
|
2658
|
+
strategy: "prescribed",
|
|
2659
|
+
strategyName: this.strategyName || this.name,
|
|
2660
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2661
|
+
action: "generated",
|
|
2662
|
+
score: BASE_PRACTICE_SCORE,
|
|
2663
|
+
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2664
|
+
}
|
|
2665
|
+
]
|
|
2666
|
+
});
|
|
2667
|
+
}
|
|
2668
|
+
return cards;
|
|
2669
|
+
}
|
|
2670
|
+
/**
|
|
2671
|
+
* True for a skill that was *gated and is now reached*: it has at least one
|
|
2672
|
+
* declared hierarchy prerequisite set, and every set is fully satisfied by the
|
|
2673
|
+
* learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
|
|
2674
|
+
* — an ungated tag was never "introduced" in the curricular sense, so it isn't
|
|
2675
|
+
* a post-intro drill target (e.g. whole-word spelling tags that share the
|
|
2676
|
+
* `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
|
|
2677
|
+
* ELO retrieval. This is the precise population the retrieval gap strands:
|
|
2678
|
+
* just-unlocked, low-ELO skills.
|
|
2679
|
+
*/
|
|
2680
|
+
isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
|
|
2681
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
2682
|
+
if (prereqSets.length === 0) return false;
|
|
2683
|
+
return prereqSets.every(
|
|
2684
|
+
(prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
2685
|
+
);
|
|
2686
|
+
}
|
|
2488
2687
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2489
2688
|
if (supportTags.length === 0) {
|
|
2490
2689
|
return [];
|
|
@@ -4279,7 +4478,7 @@ function logResultCards(cards) {
|
|
|
4279
4478
|
for (let i = 0; i < cards.length; i++) {
|
|
4280
4479
|
const c = cards[i];
|
|
4281
4480
|
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
4282
|
-
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
4481
|
+
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) => {
|
|
4283
4482
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
4284
4483
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
4285
4484
|
}).join(" | ");
|
|
@@ -4310,6 +4509,7 @@ var init_Pipeline = __esm({
|
|
|
4310
4509
|
init_logger();
|
|
4311
4510
|
init_orchestration();
|
|
4312
4511
|
init_PipelineDebugger();
|
|
4512
|
+
init_diversityRerank();
|
|
4313
4513
|
VERBOSE_RESULTS = true;
|
|
4314
4514
|
Pipeline = class extends ContentNavigator {
|
|
4315
4515
|
generator;
|
|
@@ -4483,6 +4683,7 @@ var init_Pipeline = __esm({
|
|
|
4483
4683
|
this._ephemeralHints = null;
|
|
4484
4684
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
4485
4685
|
}
|
|
4686
|
+
cards = diversityRerank(cards);
|
|
4486
4687
|
cards.sort((a, b) => b.score - a.score);
|
|
4487
4688
|
const tFilter = performance.now();
|
|
4488
4689
|
const result = cards.slice(0, limit);
|
|
@@ -4786,6 +4987,68 @@ var init_Pipeline = __esm({
|
|
|
4786
4987
|
// ---------------------------------------------------------------------------
|
|
4787
4988
|
// Card-space diagnostic
|
|
4788
4989
|
// ---------------------------------------------------------------------------
|
|
4990
|
+
/**
|
|
4991
|
+
* Commit-free forecast: score the user's full card space through the filter
|
|
4992
|
+
* chain and return the cards that are currently *reachable* (score >=
|
|
4993
|
+
* threshold), optionally nudged by caller-supplied hints and/or restricted
|
|
4994
|
+
* to cards the user hasn't seen yet.
|
|
4995
|
+
*
|
|
4996
|
+
* This is a GENERIC primitive — it returns scored, tag-hydrated cards and
|
|
4997
|
+
* stops there. It has no knowledge of any particular tag convention; callers
|
|
4998
|
+
* decide what the surviving cards mean (e.g. filter to their own "intro"
|
|
4999
|
+
* tag family). Nothing is written and no session is started.
|
|
5000
|
+
*
|
|
5001
|
+
* The optional `hints` are the "out-of-band kick": they run through the same
|
|
5002
|
+
* {@link applyHints} path a live replan uses, so the two semantics carry over —
|
|
5003
|
+
* - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
|
|
5004
|
+
* stays out), and
|
|
5005
|
+
* - `requireTags`/`requireCards` inject from the full pre-filter pool,
|
|
5006
|
+
* *bypassing* gating (use when you want a card regardless of reachability).
|
|
5007
|
+
* Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
|
|
5008
|
+
* user has already seen — pass `unseenOnly: false` if that matters.
|
|
5009
|
+
*
|
|
5010
|
+
* Cost note: like {@link diagnoseCardSpace}, this scans every card through the
|
|
5011
|
+
* filters, so it's heavier than a normal replan. Intended for one-shot
|
|
5012
|
+
* out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
|
|
5013
|
+
*
|
|
5014
|
+
* @param opts.hints Optional ephemeral hints to apply after the filter chain.
|
|
5015
|
+
* @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
|
|
5016
|
+
* @param opts.threshold Min score to count as reachable (default 0.10).
|
|
5017
|
+
* @param opts.limit Optional cap on results (already sorted desc).
|
|
5018
|
+
*/
|
|
5019
|
+
async forecast(opts) {
|
|
5020
|
+
const threshold = opts?.threshold ?? 0.1;
|
|
5021
|
+
const unseenOnly = opts?.unseenOnly ?? true;
|
|
5022
|
+
const courseId = this.course.getCourseID();
|
|
5023
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
5024
|
+
let cards = allCardIds.map((cardId) => ({
|
|
5025
|
+
cardId,
|
|
5026
|
+
courseId,
|
|
5027
|
+
score: 1,
|
|
5028
|
+
provenance: []
|
|
5029
|
+
}));
|
|
5030
|
+
cards = await this.hydrateTags(cards);
|
|
5031
|
+
const fullPool = cards.slice();
|
|
5032
|
+
const context = await this.buildContext();
|
|
5033
|
+
for (const filter of this.filters) {
|
|
5034
|
+
cards = await filter.transform(cards, context);
|
|
5035
|
+
}
|
|
5036
|
+
if (opts?.hints) {
|
|
5037
|
+
cards = this.applyHints(cards, opts.hints, fullPool);
|
|
5038
|
+
}
|
|
5039
|
+
cards = cards.filter((c) => c.score >= threshold);
|
|
5040
|
+
if (unseenOnly) {
|
|
5041
|
+
let encountered;
|
|
5042
|
+
try {
|
|
5043
|
+
encountered = new Set(await this.user.getSeenCards(courseId));
|
|
5044
|
+
} catch {
|
|
5045
|
+
encountered = /* @__PURE__ */ new Set();
|
|
5046
|
+
}
|
|
5047
|
+
cards = cards.filter((c) => !encountered.has(c.cardId));
|
|
5048
|
+
}
|
|
5049
|
+
cards.sort((a, b) => b.score - a.score);
|
|
5050
|
+
return opts?.limit ? cards.slice(0, opts.limit) : cards;
|
|
5051
|
+
}
|
|
4789
5052
|
/**
|
|
4790
5053
|
* Scan every card in the course through the filter chain and report
|
|
4791
5054
|
* how many are "well indicated" (score >= threshold) for the current user.
|
|
@@ -5051,6 +5314,7 @@ var init_3 = __esm({
|
|
|
5051
5314
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
5052
5315
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
5053
5316
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5317
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
5054
5318
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
5055
5319
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
5056
5320
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -5076,9 +5340,13 @@ var init_3 = __esm({
|
|
|
5076
5340
|
var navigators_exports = {};
|
|
5077
5341
|
__export(navigators_exports, {
|
|
5078
5342
|
ContentNavigator: () => ContentNavigator,
|
|
5343
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
5344
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
5079
5345
|
NavigatorRole: () => NavigatorRole,
|
|
5080
5346
|
NavigatorRoles: () => NavigatorRoles,
|
|
5081
5347
|
Navigators: () => Navigators,
|
|
5348
|
+
diversityRerank: () => diversityRerank,
|
|
5349
|
+
getActivePipeline: () => getActivePipeline,
|
|
5082
5350
|
getCardOrigin: () => getCardOrigin,
|
|
5083
5351
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
5084
5352
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -5162,6 +5430,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
5162
5430
|
var init_navigators = __esm({
|
|
5163
5431
|
"src/core/navigators/index.ts"() {
|
|
5164
5432
|
"use strict";
|
|
5433
|
+
init_diversityRerank();
|
|
5165
5434
|
init_PipelineDebugger();
|
|
5166
5435
|
init_logger();
|
|
5167
5436
|
init_();
|
|
@@ -11221,8 +11490,17 @@ var ItemQueue = class {
|
|
|
11221
11490
|
* Merge new items into the front of the queue, skipping duplicates.
|
|
11222
11491
|
* Used by additive replans to inject high-quality candidates without
|
|
11223
11492
|
* discarding the existing queue contents.
|
|
11493
|
+
*
|
|
11494
|
+
* `forceFrontIds` carries the mandatory (`+INF`) cards in this batch — a
|
|
11495
|
+
* durable `requireCard`/`requireTag` re-asserted by every replan. An ordinary
|
|
11496
|
+
* duplicate is left in place (skip), but a mandatory one that's *already*
|
|
11497
|
+
* queued is pulled out of its current slot so it rejoins at the front in batch
|
|
11498
|
+
* order. Without this, an additive merge unshifts fresh non-required cards
|
|
11499
|
+
* ahead of an already-present required card, steadily burying it until it never
|
|
11500
|
+
* gets drawn — defeating the "must appear" guarantee. Returns the count of
|
|
11501
|
+
* genuinely new cards added (re-fronted duplicates are not counted).
|
|
11224
11502
|
*/
|
|
11225
|
-
mergeToFront(items, cardIdExtractor) {
|
|
11503
|
+
mergeToFront(items, cardIdExtractor, forceFrontIds) {
|
|
11226
11504
|
let added = 0;
|
|
11227
11505
|
const toInsert = [];
|
|
11228
11506
|
for (const item of items) {
|
|
@@ -11231,6 +11509,11 @@ var ItemQueue = class {
|
|
|
11231
11509
|
this.seenCardIds.push(cardId);
|
|
11232
11510
|
toInsert.push(item);
|
|
11233
11511
|
added++;
|
|
11512
|
+
} else if (forceFrontIds?.has(cardId)) {
|
|
11513
|
+
const idx = this.q.findIndex((qi) => cardIdExtractor(qi) === cardId);
|
|
11514
|
+
if (idx >= 0) {
|
|
11515
|
+
toInsert.push(...this.q.splice(idx, 1));
|
|
11516
|
+
}
|
|
11234
11517
|
}
|
|
11235
11518
|
}
|
|
11236
11519
|
this.q.unshift(...toInsert);
|
|
@@ -14348,7 +14631,16 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14348
14631
|
mixedWeighted
|
|
14349
14632
|
);
|
|
14350
14633
|
const reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review").slice(0, this._initialReviewCap);
|
|
14351
|
-
const
|
|
14634
|
+
const newCandidates = mixedWeighted.filter(
|
|
14635
|
+
(w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)
|
|
14636
|
+
);
|
|
14637
|
+
const mandatoryWeighted = newCandidates.filter((w) => w.score === Number.POSITIVE_INFINITY);
|
|
14638
|
+
const optionalWeighted = newCandidates.filter((w) => w.score !== Number.POSITIVE_INFINITY);
|
|
14639
|
+
const newWeighted = [
|
|
14640
|
+
...mandatoryWeighted,
|
|
14641
|
+
...optionalWeighted.slice(0, Math.max(0, newLimit - mandatoryWeighted.length))
|
|
14642
|
+
];
|
|
14643
|
+
const mandatoryIds = new Set(mandatoryWeighted.map((w) => w.cardId));
|
|
14352
14644
|
logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
|
|
14353
14645
|
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
14354
14646
|
if (!replan) {
|
|
@@ -14383,7 +14675,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14383
14675
|
`;
|
|
14384
14676
|
}
|
|
14385
14677
|
if (additive) {
|
|
14386
|
-
const added = this.newQ.mergeToFront(newItems, (item) => item.cardID);
|
|
14678
|
+
const added = this.newQ.mergeToFront(newItems, (item) => item.cardID, mandatoryIds);
|
|
14387
14679
|
report += `Additive merge: ${added} new cards added to front of newQ
|
|
14388
14680
|
`;
|
|
14389
14681
|
} else if (replan) {
|
|
@@ -14714,6 +15006,8 @@ export {
|
|
|
14714
15006
|
ContentNavigator,
|
|
14715
15007
|
CouchDBToStaticPacker,
|
|
14716
15008
|
CourseLookup,
|
|
15009
|
+
DIVERSITY_FLOOR,
|
|
15010
|
+
DIVERSITY_STRENGTH,
|
|
14717
15011
|
DocType,
|
|
14718
15012
|
DocTypePrefixes,
|
|
14719
15013
|
ENV,
|
|
@@ -14739,9 +15033,11 @@ export {
|
|
|
14739
15033
|
computeSpread,
|
|
14740
15034
|
computeStrategyGradient,
|
|
14741
15035
|
createOrchestrationContext,
|
|
15036
|
+
diversityRerank,
|
|
14742
15037
|
docIsDeleted,
|
|
14743
15038
|
endSessionTracking,
|
|
14744
15039
|
ensureAppDataDirectory,
|
|
15040
|
+
getActivePipeline,
|
|
14745
15041
|
getAppDataDirectory,
|
|
14746
15042
|
getCardHistoryID,
|
|
14747
15043
|
getCardOrigin,
|