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