@vue-skuilder/db 0.2.5 → 0.2.7
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 +37 -1
- package/dist/core/index.d.ts +37 -1
- package/dist/core/index.js +212 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +209 -4
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +206 -4
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +206 -4
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +206 -4
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +206 -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 +238 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +235 -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 +10 -1
- package/src/core/navigators/diversityRerank.ts +185 -0
- package/src/core/navigators/generators/prescribed.ts +173 -1
- package/src/core/navigators/index.ts +8 -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,6 +849,95 @@ 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, {
|
|
@@ -2044,7 +2133,7 @@ function shuffleInPlace(arr) {
|
|
|
2044
2133
|
function pickTopByScore(cards, limit) {
|
|
2045
2134
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
2046
2135
|
}
|
|
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;
|
|
2136
|
+
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
2137
|
var init_prescribed = __esm({
|
|
2049
2138
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
2050
2139
|
"use strict";
|
|
@@ -2055,9 +2144,12 @@ var init_prescribed = __esm({
|
|
|
2055
2144
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
2056
2145
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
2057
2146
|
DEFAULT_MIN_COUNT = 3;
|
|
2147
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
2148
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
2058
2149
|
BASE_TARGET_SCORE = 1;
|
|
2059
2150
|
BASE_SUPPORT_SCORE = 0.8;
|
|
2060
2151
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
2152
|
+
BASE_PRACTICE_SCORE = 1;
|
|
2061
2153
|
MAX_TARGET_MULTIPLIER = 8;
|
|
2062
2154
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
2063
2155
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -2165,7 +2257,18 @@ var init_prescribed = __esm({
|
|
|
2165
2257
|
courseId,
|
|
2166
2258
|
emittedIds
|
|
2167
2259
|
);
|
|
2168
|
-
|
|
2260
|
+
const practiceCards = this.buildPracticeCards({
|
|
2261
|
+
group,
|
|
2262
|
+
courseId,
|
|
2263
|
+
emittedIds,
|
|
2264
|
+
cardsByTag,
|
|
2265
|
+
hierarchyConfigs,
|
|
2266
|
+
userTagElo,
|
|
2267
|
+
userGlobalElo,
|
|
2268
|
+
activeIds,
|
|
2269
|
+
seenIds
|
|
2270
|
+
});
|
|
2271
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
2169
2272
|
}
|
|
2170
2273
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2171
2274
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -2193,6 +2296,7 @@ var init_prescribed = __esm({
|
|
|
2193
2296
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
2194
2297
|
for (const card of finalCards) {
|
|
2195
2298
|
const prov = card.provenance[0];
|
|
2299
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
2196
2300
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
2197
2301
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
2198
2302
|
if (!groupId) continue;
|
|
@@ -2262,7 +2366,12 @@ var init_prescribed = __esm({
|
|
|
2262
2366
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
2263
2367
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
2264
2368
|
},
|
|
2265
|
-
retireOnEncounter: raw.retireOnEncounter !== false
|
|
2369
|
+
retireOnEncounter: raw.retireOnEncounter !== false,
|
|
2370
|
+
practiceTagPatterns: dedupe(
|
|
2371
|
+
Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
|
|
2372
|
+
),
|
|
2373
|
+
practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
|
|
2374
|
+
maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
|
|
2266
2375
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
2267
2376
|
return { groups };
|
|
2268
2377
|
} catch {
|
|
@@ -2485,6 +2594,92 @@ var init_prescribed = __esm({
|
|
|
2485
2594
|
}
|
|
2486
2595
|
return cards;
|
|
2487
2596
|
}
|
|
2597
|
+
/**
|
|
2598
|
+
* Emit drill cards for *unlocked-but-under-practiced* skills.
|
|
2599
|
+
*
|
|
2600
|
+
* For each course tag matching the group's `practiceTagPatterns` that is both
|
|
2601
|
+
* unlocked (all hierarchy prerequisites met — i.e. the learner has been
|
|
2602
|
+
* introduced to it) and under-practiced (per-tag attempt count below
|
|
2603
|
+
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2604
|
+
* into the candidate pool. It exists because global-ELO retrieval
|
|
2605
|
+
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2606
|
+
* freshly-introduced skill — putting them in the pool here lets the pipeline's
|
|
2607
|
+
* scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
|
|
2608
|
+
* this method's job; it only guarantees presence.
|
|
2609
|
+
*
|
|
2610
|
+
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2611
|
+
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
2612
|
+
*/
|
|
2613
|
+
buildPracticeCards(args) {
|
|
2614
|
+
const {
|
|
2615
|
+
group,
|
|
2616
|
+
courseId,
|
|
2617
|
+
emittedIds,
|
|
2618
|
+
cardsByTag,
|
|
2619
|
+
hierarchyConfigs,
|
|
2620
|
+
userTagElo,
|
|
2621
|
+
userGlobalElo,
|
|
2622
|
+
activeIds,
|
|
2623
|
+
seenIds
|
|
2624
|
+
} = args;
|
|
2625
|
+
const patterns = group.practiceTagPatterns ?? [];
|
|
2626
|
+
if (patterns.length === 0) return [];
|
|
2627
|
+
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2628
|
+
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2629
|
+
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2630
|
+
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2631
|
+
);
|
|
2632
|
+
if (practiceTags.length === 0) return [];
|
|
2633
|
+
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2634
|
+
supportTags: practiceTags,
|
|
2635
|
+
cardsByTag,
|
|
2636
|
+
activeIds,
|
|
2637
|
+
seenIds,
|
|
2638
|
+
excludedIds: emittedIds,
|
|
2639
|
+
limit: maxPractice
|
|
2640
|
+
});
|
|
2641
|
+
if (practiceCardIds.length === 0) return [];
|
|
2642
|
+
logger.info(
|
|
2643
|
+
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2644
|
+
);
|
|
2645
|
+
const cards = [];
|
|
2646
|
+
for (const cardId of practiceCardIds) {
|
|
2647
|
+
emittedIds.add(cardId);
|
|
2648
|
+
cards.push({
|
|
2649
|
+
cardId,
|
|
2650
|
+
courseId,
|
|
2651
|
+
score: BASE_PRACTICE_SCORE,
|
|
2652
|
+
provenance: [
|
|
2653
|
+
{
|
|
2654
|
+
strategy: "prescribed",
|
|
2655
|
+
strategyName: this.strategyName || this.name,
|
|
2656
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2657
|
+
action: "generated",
|
|
2658
|
+
score: BASE_PRACTICE_SCORE,
|
|
2659
|
+
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2660
|
+
}
|
|
2661
|
+
]
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
return cards;
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2667
|
+
* True for a skill that was *gated and is now reached*: it has at least one
|
|
2668
|
+
* declared hierarchy prerequisite set, and every set is fully satisfied by the
|
|
2669
|
+
* learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
|
|
2670
|
+
* — an ungated tag was never "introduced" in the curricular sense, so it isn't
|
|
2671
|
+
* a post-intro drill target (e.g. whole-word spelling tags that share the
|
|
2672
|
+
* `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
|
|
2673
|
+
* ELO retrieval. This is the precise population the retrieval gap strands:
|
|
2674
|
+
* just-unlocked, low-ELO skills.
|
|
2675
|
+
*/
|
|
2676
|
+
isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
|
|
2677
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
2678
|
+
if (prereqSets.length === 0) return false;
|
|
2679
|
+
return prereqSets.every(
|
|
2680
|
+
(prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
2681
|
+
);
|
|
2682
|
+
}
|
|
2488
2683
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2489
2684
|
if (supportTags.length === 0) {
|
|
2490
2685
|
return [];
|
|
@@ -4279,7 +4474,7 @@ function logResultCards(cards) {
|
|
|
4279
4474
|
for (let i = 0; i < cards.length; i++) {
|
|
4280
4475
|
const c = cards[i];
|
|
4281
4476
|
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) => {
|
|
4477
|
+
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
4478
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
4284
4479
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
4285
4480
|
}).join(" | ");
|
|
@@ -4310,6 +4505,7 @@ var init_Pipeline = __esm({
|
|
|
4310
4505
|
init_logger();
|
|
4311
4506
|
init_orchestration();
|
|
4312
4507
|
init_PipelineDebugger();
|
|
4508
|
+
init_diversityRerank();
|
|
4313
4509
|
VERBOSE_RESULTS = true;
|
|
4314
4510
|
Pipeline = class extends ContentNavigator {
|
|
4315
4511
|
generator;
|
|
@@ -4483,6 +4679,7 @@ var init_Pipeline = __esm({
|
|
|
4483
4679
|
this._ephemeralHints = null;
|
|
4484
4680
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
4485
4681
|
}
|
|
4682
|
+
cards = diversityRerank(cards);
|
|
4486
4683
|
cards.sort((a, b) => b.score - a.score);
|
|
4487
4684
|
const tFilter = performance.now();
|
|
4488
4685
|
const result = cards.slice(0, limit);
|
|
@@ -5051,6 +5248,7 @@ var init_3 = __esm({
|
|
|
5051
5248
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
5052
5249
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
5053
5250
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5251
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
5054
5252
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
5055
5253
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
5056
5254
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -5076,9 +5274,12 @@ var init_3 = __esm({
|
|
|
5076
5274
|
var navigators_exports = {};
|
|
5077
5275
|
__export(navigators_exports, {
|
|
5078
5276
|
ContentNavigator: () => ContentNavigator,
|
|
5277
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
5278
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
5079
5279
|
NavigatorRole: () => NavigatorRole,
|
|
5080
5280
|
NavigatorRoles: () => NavigatorRoles,
|
|
5081
5281
|
Navigators: () => Navigators,
|
|
5282
|
+
diversityRerank: () => diversityRerank,
|
|
5082
5283
|
getCardOrigin: () => getCardOrigin,
|
|
5083
5284
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
5084
5285
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -5162,6 +5363,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
5162
5363
|
var init_navigators = __esm({
|
|
5163
5364
|
"src/core/navigators/index.ts"() {
|
|
5164
5365
|
"use strict";
|
|
5366
|
+
init_diversityRerank();
|
|
5165
5367
|
init_PipelineDebugger();
|
|
5166
5368
|
init_logger();
|
|
5167
5369
|
init_();
|
|
@@ -11221,8 +11423,17 @@ var ItemQueue = class {
|
|
|
11221
11423
|
* Merge new items into the front of the queue, skipping duplicates.
|
|
11222
11424
|
* Used by additive replans to inject high-quality candidates without
|
|
11223
11425
|
* discarding the existing queue contents.
|
|
11426
|
+
*
|
|
11427
|
+
* `forceFrontIds` carries the mandatory (`+INF`) cards in this batch — a
|
|
11428
|
+
* durable `requireCard`/`requireTag` re-asserted by every replan. An ordinary
|
|
11429
|
+
* duplicate is left in place (skip), but a mandatory one that's *already*
|
|
11430
|
+
* queued is pulled out of its current slot so it rejoins at the front in batch
|
|
11431
|
+
* order. Without this, an additive merge unshifts fresh non-required cards
|
|
11432
|
+
* ahead of an already-present required card, steadily burying it until it never
|
|
11433
|
+
* gets drawn — defeating the "must appear" guarantee. Returns the count of
|
|
11434
|
+
* genuinely new cards added (re-fronted duplicates are not counted).
|
|
11224
11435
|
*/
|
|
11225
|
-
mergeToFront(items, cardIdExtractor) {
|
|
11436
|
+
mergeToFront(items, cardIdExtractor, forceFrontIds) {
|
|
11226
11437
|
let added = 0;
|
|
11227
11438
|
const toInsert = [];
|
|
11228
11439
|
for (const item of items) {
|
|
@@ -11231,6 +11442,11 @@ var ItemQueue = class {
|
|
|
11231
11442
|
this.seenCardIds.push(cardId);
|
|
11232
11443
|
toInsert.push(item);
|
|
11233
11444
|
added++;
|
|
11445
|
+
} else if (forceFrontIds?.has(cardId)) {
|
|
11446
|
+
const idx = this.q.findIndex((qi) => cardIdExtractor(qi) === cardId);
|
|
11447
|
+
if (idx >= 0) {
|
|
11448
|
+
toInsert.push(...this.q.splice(idx, 1));
|
|
11449
|
+
}
|
|
11234
11450
|
}
|
|
11235
11451
|
}
|
|
11236
11452
|
this.q.unshift(...toInsert);
|
|
@@ -14348,7 +14564,16 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14348
14564
|
mixedWeighted
|
|
14349
14565
|
);
|
|
14350
14566
|
const reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review").slice(0, this._initialReviewCap);
|
|
14351
|
-
const
|
|
14567
|
+
const newCandidates = mixedWeighted.filter(
|
|
14568
|
+
(w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)
|
|
14569
|
+
);
|
|
14570
|
+
const mandatoryWeighted = newCandidates.filter((w) => w.score === Number.POSITIVE_INFINITY);
|
|
14571
|
+
const optionalWeighted = newCandidates.filter((w) => w.score !== Number.POSITIVE_INFINITY);
|
|
14572
|
+
const newWeighted = [
|
|
14573
|
+
...mandatoryWeighted,
|
|
14574
|
+
...optionalWeighted.slice(0, Math.max(0, newLimit - mandatoryWeighted.length))
|
|
14575
|
+
];
|
|
14576
|
+
const mandatoryIds = new Set(mandatoryWeighted.map((w) => w.cardId));
|
|
14352
14577
|
logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
|
|
14353
14578
|
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
14354
14579
|
if (!replan) {
|
|
@@ -14383,7 +14608,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14383
14608
|
`;
|
|
14384
14609
|
}
|
|
14385
14610
|
if (additive) {
|
|
14386
|
-
const added = this.newQ.mergeToFront(newItems, (item) => item.cardID);
|
|
14611
|
+
const added = this.newQ.mergeToFront(newItems, (item) => item.cardID, mandatoryIds);
|
|
14387
14612
|
report += `Additive merge: ${added} new cards added to front of newQ
|
|
14388
14613
|
`;
|
|
14389
14614
|
} else if (replan) {
|
|
@@ -14714,6 +14939,8 @@ export {
|
|
|
14714
14939
|
ContentNavigator,
|
|
14715
14940
|
CouchDBToStaticPacker,
|
|
14716
14941
|
CourseLookup,
|
|
14942
|
+
DIVERSITY_FLOOR,
|
|
14943
|
+
DIVERSITY_STRENGTH,
|
|
14717
14944
|
DocType,
|
|
14718
14945
|
DocTypePrefixes,
|
|
14719
14946
|
ENV,
|
|
@@ -14739,6 +14966,7 @@ export {
|
|
|
14739
14966
|
computeSpread,
|
|
14740
14967
|
computeStrategyGradient,
|
|
14741
14968
|
createOrchestrationContext,
|
|
14969
|
+
diversityRerank,
|
|
14742
14970
|
docIsDeleted,
|
|
14743
14971
|
endSessionTracking,
|
|
14744
14972
|
ensureAppDataDirectory,
|