@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/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
- emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
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 newWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)).slice(0, newLimit);
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,