@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/index.d.cts CHANGED
@@ -4,7 +4,7 @@ import { D as DataLayerProvider } from './dataLayerProvider-CiA2Rr0v.cjs';
4
4
  import { C as CardHistory, c as CardRecord, d as QuestionRecord } from './types-legacy-4tlwHnXo.cjs';
5
5
  export { e as CardData, f as CourseListData, h as DataShapeData, g as DisplayableData, D as DocType, b as DocTypePrefixes, F as Field, G as GuestUsername, Q as QualifiedCardID, i as QuestionData, S as SkuilderCourseData, a as Tag, T as TagStub, l as log } from './types-legacy-4tlwHnXo.cjs';
6
6
  import { Loggable } from './core/index.cjs';
7
- export { BulkCardProcessorConfig, CardFilter, CardFilterFactory, FilterContext, FilterImpact, GeneratorSummary, GradientObservation, GradientResult, ImportResult, PeriodUpdateInput, PeriodUpdateResult, PipelineRunReport, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig } from './core/index.cjs';
7
+ export { BulkCardProcessorConfig, CardFilter, CardFilterFactory, DIVERSITY_FLOOR, DIVERSITY_STRENGTH, DiversityRerankOptions, FilterContext, FilterImpact, GeneratorSummary, GradientObservation, GradientResult, ImportResult, PeriodUpdateInput, PeriodUpdateResult, PipelineRunReport, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, diversityRerank, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig } from './core/index.cjs';
8
8
  import { TaggedPerformance, TagFilter, DataShape, CourseConfig } from '@vue-skuilder/common';
9
9
  import { S as StaticCourseManifest } from './types-BFUa1pa3.cjs';
10
10
  export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig } from './types-BFUa1pa3.cjs';
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ import { D as DataLayerProvider } from './dataLayerProvider-DrBqOUa3.js';
4
4
  import { C as CardHistory, c as CardRecord, d as QuestionRecord } from './types-legacy-4tlwHnXo.js';
5
5
  export { e as CardData, f as CourseListData, h as DataShapeData, g as DisplayableData, D as DocType, b as DocTypePrefixes, F as Field, G as GuestUsername, Q as QualifiedCardID, i as QuestionData, S as SkuilderCourseData, a as Tag, T as TagStub, l as log } from './types-legacy-4tlwHnXo.js';
6
6
  import { Loggable } from './core/index.js';
7
- export { BulkCardProcessorConfig, CardFilter, CardFilterFactory, FilterContext, FilterImpact, GeneratorSummary, GradientObservation, GradientResult, ImportResult, PeriodUpdateInput, PeriodUpdateResult, PipelineRunReport, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig } from './core/index.js';
7
+ export { BulkCardProcessorConfig, CardFilter, CardFilterFactory, DIVERSITY_FLOOR, DIVERSITY_STRENGTH, DiversityRerankOptions, FilterContext, FilterImpact, GeneratorSummary, GradientObservation, GradientResult, ImportResult, PeriodUpdateInput, PeriodUpdateResult, PipelineRunReport, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, diversityRerank, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig } from './core/index.js';
8
8
  import { TaggedPerformance, TagFilter, DataShape, CourseConfig } from '@vue-skuilder/common';
9
9
  import { S as StaticCourseManifest } from './types-CHgpWQAY.js';
10
10
  export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig } from './types-CHgpWQAY.js';
package/dist/index.js CHANGED
@@ -872,6 +872,95 @@ var init_courseLookupDB = __esm({
872
872
  }
873
873
  });
874
874
 
875
+ // src/core/navigators/diversityRerank.ts
876
+ var diversityRerank_exports = {};
877
+ __export(diversityRerank_exports, {
878
+ DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
879
+ DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
880
+ diversityRerank: () => diversityRerank
881
+ });
882
+ function diversityRerank(cards, opts = {}) {
883
+ const strength = opts.strength ?? DIVERSITY_STRENGTH;
884
+ const floor = opts.floor ?? DIVERSITY_FLOOR;
885
+ const n = cards.length;
886
+ if (n <= 1) return cards;
887
+ const df = /* @__PURE__ */ new Map();
888
+ for (const card of cards) {
889
+ for (const tag of card.tags ?? []) {
890
+ df.set(tag, (df.get(tag) ?? 0) + 1);
891
+ }
892
+ }
893
+ const idf = /* @__PURE__ */ new Map();
894
+ for (const [tag, freq] of df) {
895
+ idf.set(tag, Math.log(n / freq));
896
+ }
897
+ const remaining = [...cards];
898
+ const emittedCount = /* @__PURE__ */ new Map();
899
+ const out = [];
900
+ const repetitionLoad = (card) => {
901
+ let load = 0;
902
+ for (const tag of card.tags ?? []) {
903
+ const seen = emittedCount.get(tag);
904
+ if (seen) load += (idf.get(tag) ?? 0) * seen;
905
+ }
906
+ return load;
907
+ };
908
+ while (remaining.length > 0) {
909
+ let bestIdx = 0;
910
+ let bestValue = -Infinity;
911
+ let bestPenalty = 1;
912
+ let bestLoad = 0;
913
+ for (let i = 0; i < remaining.length; i++) {
914
+ const card = remaining[i];
915
+ const load = repetitionLoad(card);
916
+ const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
917
+ const value = card.score * penalty;
918
+ if (value > bestValue) {
919
+ bestValue = value;
920
+ bestIdx = i;
921
+ bestPenalty = penalty;
922
+ bestLoad = load;
923
+ }
924
+ }
925
+ const [picked] = remaining.splice(bestIdx, 1);
926
+ if (Number.isFinite(picked.score) && bestPenalty < 1) {
927
+ const newScore = picked.score * bestPenalty;
928
+ out.push({
929
+ ...picked,
930
+ score: newScore,
931
+ provenance: [
932
+ ...picked.provenance,
933
+ {
934
+ strategy: STRATEGY,
935
+ strategyId: STRATEGY_ID,
936
+ strategyName: STRATEGY_NAME,
937
+ action: "penalized",
938
+ score: newScore,
939
+ reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
940
+ }
941
+ ]
942
+ });
943
+ } else {
944
+ out.push(picked);
945
+ }
946
+ for (const tag of picked.tags ?? []) {
947
+ emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
948
+ }
949
+ }
950
+ return out;
951
+ }
952
+ var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
953
+ var init_diversityRerank = __esm({
954
+ "src/core/navigators/diversityRerank.ts"() {
955
+ "use strict";
956
+ DIVERSITY_STRENGTH = 0.6;
957
+ DIVERSITY_FLOOR = 0.3;
958
+ STRATEGY = "diversityRerank";
959
+ STRATEGY_ID = "DIVERSITY_RERANK";
960
+ STRATEGY_NAME = "Diversity Re-rank";
961
+ }
962
+ });
963
+
875
964
  // src/core/navigators/PipelineDebugger.ts
876
965
  var PipelineDebugger_exports = {};
877
966
  __export(PipelineDebugger_exports, {
@@ -2067,7 +2156,7 @@ function shuffleInPlace(arr) {
2067
2156
  function pickTopByScore(cards, limit) {
2068
2157
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
2069
2158
  }
2070
- 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;
2159
+ 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;
2071
2160
  var init_prescribed = __esm({
2072
2161
  "src/core/navigators/generators/prescribed.ts"() {
2073
2162
  "use strict";
@@ -2078,9 +2167,12 @@ var init_prescribed = __esm({
2078
2167
  DEFAULT_MAX_SUPPORT_PER_RUN = 3;
2079
2168
  DEFAULT_HIERARCHY_DEPTH = 2;
2080
2169
  DEFAULT_MIN_COUNT = 3;
2170
+ DEFAULT_PRACTICE_MIN_COUNT = 3;
2171
+ DEFAULT_MAX_PRACTICE_PER_RUN = 4;
2081
2172
  BASE_TARGET_SCORE = 1;
2082
2173
  BASE_SUPPORT_SCORE = 0.8;
2083
2174
  DISCOVERED_SUPPORT_SCORE = 12;
2175
+ BASE_PRACTICE_SCORE = 1;
2084
2176
  MAX_TARGET_MULTIPLIER = 8;
2085
2177
  MAX_SUPPORT_MULTIPLIER = 4;
2086
2178
  PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
@@ -2188,7 +2280,18 @@ var init_prescribed = __esm({
2188
2280
  courseId,
2189
2281
  emittedIds
2190
2282
  );
2191
- emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
2283
+ const practiceCards = this.buildPracticeCards({
2284
+ group,
2285
+ courseId,
2286
+ emittedIds,
2287
+ cardsByTag,
2288
+ hierarchyConfigs,
2289
+ userTagElo,
2290
+ userGlobalElo,
2291
+ activeIds,
2292
+ seenIds
2293
+ });
2294
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
2192
2295
  }
2193
2296
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
2194
2297
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
@@ -2216,6 +2319,7 @@ var init_prescribed = __esm({
2216
2319
  const surfacedByGroup = /* @__PURE__ */ new Map();
2217
2320
  for (const card of finalCards) {
2218
2321
  const prov = card.provenance[0];
2322
+ if (prov?.reason.includes("mode=practice")) continue;
2219
2323
  const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
2220
2324
  const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
2221
2325
  if (!groupId) continue;
@@ -2285,7 +2389,12 @@ var init_prescribed = __esm({
2285
2389
  enabled: raw.hierarchyWalk?.enabled !== false,
2286
2390
  maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
2287
2391
  },
2288
- retireOnEncounter: raw.retireOnEncounter !== false
2392
+ retireOnEncounter: raw.retireOnEncounter !== false,
2393
+ practiceTagPatterns: dedupe(
2394
+ Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
2395
+ ),
2396
+ practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
2397
+ maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
2289
2398
  })).filter((g) => g.targetCardIds.length > 0);
2290
2399
  return { groups };
2291
2400
  } catch {
@@ -2508,6 +2617,92 @@ var init_prescribed = __esm({
2508
2617
  }
2509
2618
  return cards;
2510
2619
  }
2620
+ /**
2621
+ * Emit drill cards for *unlocked-but-under-practiced* skills.
2622
+ *
2623
+ * For each course tag matching the group's `practiceTagPatterns` that is both
2624
+ * unlocked (all hierarchy prerequisites met — i.e. the learner has been
2625
+ * introduced to it) and under-practiced (per-tag attempt count below
2626
+ * `practiceMinCount`), this resolves cards carrying that tag and emits them
2627
+ * into the candidate pool. It exists because global-ELO retrieval
2628
+ * systematically fails to fetch the (low-ELO) drill cards for a
2629
+ * freshly-introduced skill — putting them in the pool here lets the pipeline's
2630
+ * scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
2631
+ * this method's job; it only guarantees presence.
2632
+ *
2633
+ * Fully data-driven: the unlock relation comes from the hierarchy config and
2634
+ * practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
2635
+ */
2636
+ buildPracticeCards(args) {
2637
+ const {
2638
+ group,
2639
+ courseId,
2640
+ emittedIds,
2641
+ cardsByTag,
2642
+ hierarchyConfigs,
2643
+ userTagElo,
2644
+ userGlobalElo,
2645
+ activeIds,
2646
+ seenIds
2647
+ } = args;
2648
+ const patterns = group.practiceTagPatterns ?? [];
2649
+ if (patterns.length === 0) return [];
2650
+ const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
2651
+ const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
2652
+ const practiceTags = [...cardsByTag.keys()].filter(
2653
+ (tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
2654
+ );
2655
+ if (practiceTags.length === 0) return [];
2656
+ const practiceCardIds = this.findDiscoveredSupportCards({
2657
+ supportTags: practiceTags,
2658
+ cardsByTag,
2659
+ activeIds,
2660
+ seenIds,
2661
+ excludedIds: emittedIds,
2662
+ limit: maxPractice
2663
+ });
2664
+ if (practiceCardIds.length === 0) return [];
2665
+ logger.info(
2666
+ `[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
2667
+ );
2668
+ const cards = [];
2669
+ for (const cardId of practiceCardIds) {
2670
+ emittedIds.add(cardId);
2671
+ cards.push({
2672
+ cardId,
2673
+ courseId,
2674
+ score: BASE_PRACTICE_SCORE,
2675
+ provenance: [
2676
+ {
2677
+ strategy: "prescribed",
2678
+ strategyName: this.strategyName || this.name,
2679
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2680
+ action: "generated",
2681
+ score: BASE_PRACTICE_SCORE,
2682
+ reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
2683
+ }
2684
+ ]
2685
+ });
2686
+ }
2687
+ return cards;
2688
+ }
2689
+ /**
2690
+ * True for a skill that was *gated and is now reached*: it has at least one
2691
+ * declared hierarchy prerequisite set, and every set is fully satisfied by the
2692
+ * learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
2693
+ * — an ungated tag was never "introduced" in the curricular sense, so it isn't
2694
+ * a post-intro drill target (e.g. whole-word spelling tags that share the
2695
+ * `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
2696
+ * ELO retrieval. This is the precise population the retrieval gap strands:
2697
+ * just-unlocked, low-ELO skills.
2698
+ */
2699
+ isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
2700
+ const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
2701
+ if (prereqSets.length === 0) return false;
2702
+ return prereqSets.every(
2703
+ (prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
2704
+ );
2705
+ }
2511
2706
  findSupportCardsByTags(group, tagsByCard, supportTags) {
2512
2707
  if (supportTags.length === 0) {
2513
2708
  return [];
@@ -4301,7 +4496,7 @@ function logResultCards(cards) {
4301
4496
  for (let i = 0; i < cards.length; i++) {
4302
4497
  const c = cards[i];
4303
4498
  const tags = c.tags?.slice(0, 3).join(", ") || "";
4304
- const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
4499
+ 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) => {
4305
4500
  const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
4306
4501
  return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
4307
4502
  }).join(" | ");
@@ -4333,6 +4528,7 @@ var init_Pipeline = __esm({
4333
4528
  init_logger();
4334
4529
  init_orchestration();
4335
4530
  init_PipelineDebugger();
4531
+ init_diversityRerank();
4336
4532
  VERBOSE_RESULTS = true;
4337
4533
  Pipeline = class extends ContentNavigator {
4338
4534
  generator;
@@ -4506,6 +4702,7 @@ var init_Pipeline = __esm({
4506
4702
  this._ephemeralHints = null;
4507
4703
  cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
4508
4704
  }
4705
+ cards = diversityRerank(cards);
4509
4706
  cards.sort((a, b) => b.score - a.score);
4510
4707
  const tFilter = performance.now();
4511
4708
  const result = cards.slice(0, limit);
@@ -5074,6 +5271,7 @@ var init_3 = __esm({
5074
5271
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
5075
5272
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
5076
5273
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
5274
+ "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
5077
5275
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
5078
5276
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
5079
5277
  "./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
@@ -5099,9 +5297,12 @@ var init_3 = __esm({
5099
5297
  var navigators_exports = {};
5100
5298
  __export(navigators_exports, {
5101
5299
  ContentNavigator: () => ContentNavigator,
5300
+ DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
5301
+ DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
5102
5302
  NavigatorRole: () => NavigatorRole,
5103
5303
  NavigatorRoles: () => NavigatorRoles,
5104
5304
  Navigators: () => Navigators,
5305
+ diversityRerank: () => diversityRerank,
5105
5306
  getCardOrigin: () => getCardOrigin,
5106
5307
  getRegisteredNavigator: () => getRegisteredNavigator,
5107
5308
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
@@ -5185,6 +5386,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
5185
5386
  var init_navigators = __esm({
5186
5387
  "src/core/navigators/index.ts"() {
5187
5388
  "use strict";
5389
+ init_diversityRerank();
5188
5390
  init_PipelineDebugger();
5189
5391
  init_logger();
5190
5392
  init_();
@@ -10187,6 +10389,8 @@ __export(index_exports, {
10187
10389
  ContentNavigator: () => ContentNavigator,
10188
10390
  CouchDBToStaticPacker: () => CouchDBToStaticPacker,
10189
10391
  CourseLookup: () => CourseLookup,
10392
+ DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
10393
+ DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
10190
10394
  DocType: () => DocType,
10191
10395
  DocTypePrefixes: () => DocTypePrefixes,
10192
10396
  ENV: () => ENV,
@@ -10212,6 +10416,7 @@ __export(index_exports, {
10212
10416
  computeSpread: () => computeSpread,
10213
10417
  computeStrategyGradient: () => computeStrategyGradient,
10214
10418
  createOrchestrationContext: () => createOrchestrationContext,
10419
+ diversityRerank: () => diversityRerank,
10215
10420
  docIsDeleted: () => docIsDeleted,
10216
10421
  endSessionTracking: () => endSessionTracking,
10217
10422
  ensureAppDataDirectory: () => ensureAppDataDirectory,
@@ -11323,8 +11528,17 @@ var ItemQueue = class {
11323
11528
  * Merge new items into the front of the queue, skipping duplicates.
11324
11529
  * Used by additive replans to inject high-quality candidates without
11325
11530
  * discarding the existing queue contents.
11531
+ *
11532
+ * `forceFrontIds` carries the mandatory (`+INF`) cards in this batch — a
11533
+ * durable `requireCard`/`requireTag` re-asserted by every replan. An ordinary
11534
+ * duplicate is left in place (skip), but a mandatory one that's *already*
11535
+ * queued is pulled out of its current slot so it rejoins at the front in batch
11536
+ * order. Without this, an additive merge unshifts fresh non-required cards
11537
+ * ahead of an already-present required card, steadily burying it until it never
11538
+ * gets drawn — defeating the "must appear" guarantee. Returns the count of
11539
+ * genuinely new cards added (re-fronted duplicates are not counted).
11326
11540
  */
11327
- mergeToFront(items, cardIdExtractor) {
11541
+ mergeToFront(items, cardIdExtractor, forceFrontIds) {
11328
11542
  let added = 0;
11329
11543
  const toInsert = [];
11330
11544
  for (const item of items) {
@@ -11333,6 +11547,11 @@ var ItemQueue = class {
11333
11547
  this.seenCardIds.push(cardId);
11334
11548
  toInsert.push(item);
11335
11549
  added++;
11550
+ } else if (forceFrontIds?.has(cardId)) {
11551
+ const idx = this.q.findIndex((qi) => cardIdExtractor(qi) === cardId);
11552
+ if (idx >= 0) {
11553
+ toInsert.push(...this.q.splice(idx, 1));
11554
+ }
11336
11555
  }
11337
11556
  }
11338
11557
  this.q.unshift(...toInsert);
@@ -14450,7 +14669,16 @@ var SessionController = class _SessionController extends Loggable {
14450
14669
  mixedWeighted
14451
14670
  );
14452
14671
  const reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review").slice(0, this._initialReviewCap);
14453
- const newWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)).slice(0, newLimit);
14672
+ const newCandidates = mixedWeighted.filter(
14673
+ (w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)
14674
+ );
14675
+ const mandatoryWeighted = newCandidates.filter((w) => w.score === Number.POSITIVE_INFINITY);
14676
+ const optionalWeighted = newCandidates.filter((w) => w.score !== Number.POSITIVE_INFINITY);
14677
+ const newWeighted = [
14678
+ ...mandatoryWeighted,
14679
+ ...optionalWeighted.slice(0, Math.max(0, newLimit - mandatoryWeighted.length))
14680
+ ];
14681
+ const mandatoryIds = new Set(mandatoryWeighted.map((w) => w.cardId));
14454
14682
  logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
14455
14683
  let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
14456
14684
  if (!replan) {
@@ -14485,7 +14713,7 @@ var SessionController = class _SessionController extends Loggable {
14485
14713
  `;
14486
14714
  }
14487
14715
  if (additive) {
14488
- const added = this.newQ.mergeToFront(newItems, (item) => item.cardID);
14716
+ const added = this.newQ.mergeToFront(newItems, (item) => item.cardID, mandatoryIds);
14489
14717
  report += `Additive merge: ${added} new cards added to front of newQ
14490
14718
  `;
14491
14719
  } else if (replan) {
@@ -14817,6 +15045,8 @@ init_factory();
14817
15045
  ContentNavigator,
14818
15046
  CouchDBToStaticPacker,
14819
15047
  CourseLookup,
15048
+ DIVERSITY_FLOOR,
15049
+ DIVERSITY_STRENGTH,
14820
15050
  DocType,
14821
15051
  DocTypePrefixes,
14822
15052
  ENV,
@@ -14842,6 +15072,7 @@ init_factory();
14842
15072
  computeSpread,
14843
15073
  computeStrategyGradient,
14844
15074
  createOrchestrationContext,
15075
+ diversityRerank,
14845
15076
  docIsDeleted,
14846
15077
  endSessionTracking,
14847
15078
  ensureAppDataDirectory,