@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.
@@ -696,6 +696,95 @@ var init_courseLookupDB = __esm({
696
696
  }
697
697
  });
698
698
 
699
+ // src/core/navigators/diversityRerank.ts
700
+ var diversityRerank_exports = {};
701
+ __export(diversityRerank_exports, {
702
+ DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
703
+ DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
704
+ diversityRerank: () => diversityRerank
705
+ });
706
+ function diversityRerank(cards, opts = {}) {
707
+ const strength = opts.strength ?? DIVERSITY_STRENGTH;
708
+ const floor = opts.floor ?? DIVERSITY_FLOOR;
709
+ const n = cards.length;
710
+ if (n <= 1) return cards;
711
+ const df = /* @__PURE__ */ new Map();
712
+ for (const card of cards) {
713
+ for (const tag of card.tags ?? []) {
714
+ df.set(tag, (df.get(tag) ?? 0) + 1);
715
+ }
716
+ }
717
+ const idf = /* @__PURE__ */ new Map();
718
+ for (const [tag, freq] of df) {
719
+ idf.set(tag, Math.log(n / freq));
720
+ }
721
+ const remaining = [...cards];
722
+ const emittedCount = /* @__PURE__ */ new Map();
723
+ const out = [];
724
+ const repetitionLoad = (card) => {
725
+ let load = 0;
726
+ for (const tag of card.tags ?? []) {
727
+ const seen = emittedCount.get(tag);
728
+ if (seen) load += (idf.get(tag) ?? 0) * seen;
729
+ }
730
+ return load;
731
+ };
732
+ while (remaining.length > 0) {
733
+ let bestIdx = 0;
734
+ let bestValue = -Infinity;
735
+ let bestPenalty = 1;
736
+ let bestLoad = 0;
737
+ for (let i = 0; i < remaining.length; i++) {
738
+ const card = remaining[i];
739
+ const load = repetitionLoad(card);
740
+ const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
741
+ const value = card.score * penalty;
742
+ if (value > bestValue) {
743
+ bestValue = value;
744
+ bestIdx = i;
745
+ bestPenalty = penalty;
746
+ bestLoad = load;
747
+ }
748
+ }
749
+ const [picked] = remaining.splice(bestIdx, 1);
750
+ if (Number.isFinite(picked.score) && bestPenalty < 1) {
751
+ const newScore = picked.score * bestPenalty;
752
+ out.push({
753
+ ...picked,
754
+ score: newScore,
755
+ provenance: [
756
+ ...picked.provenance,
757
+ {
758
+ strategy: STRATEGY,
759
+ strategyId: STRATEGY_ID,
760
+ strategyName: STRATEGY_NAME,
761
+ action: "penalized",
762
+ score: newScore,
763
+ reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
764
+ }
765
+ ]
766
+ });
767
+ } else {
768
+ out.push(picked);
769
+ }
770
+ for (const tag of picked.tags ?? []) {
771
+ emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
772
+ }
773
+ }
774
+ return out;
775
+ }
776
+ var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
777
+ var init_diversityRerank = __esm({
778
+ "src/core/navigators/diversityRerank.ts"() {
779
+ "use strict";
780
+ DIVERSITY_STRENGTH = 0.6;
781
+ DIVERSITY_FLOOR = 0.3;
782
+ STRATEGY = "diversityRerank";
783
+ STRATEGY_ID = "DIVERSITY_RERANK";
784
+ STRATEGY_NAME = "Diversity Re-rank";
785
+ }
786
+ });
787
+
699
788
  // src/core/navigators/PipelineDebugger.ts
700
789
  var PipelineDebugger_exports = {};
701
790
  __export(PipelineDebugger_exports, {
@@ -1891,7 +1980,7 @@ function shuffleInPlace(arr) {
1891
1980
  function pickTopByScore(cards, limit) {
1892
1981
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1893
1982
  }
1894
- 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;
1983
+ 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;
1895
1984
  var init_prescribed = __esm({
1896
1985
  "src/core/navigators/generators/prescribed.ts"() {
1897
1986
  "use strict";
@@ -1902,9 +1991,12 @@ var init_prescribed = __esm({
1902
1991
  DEFAULT_MAX_SUPPORT_PER_RUN = 3;
1903
1992
  DEFAULT_HIERARCHY_DEPTH = 2;
1904
1993
  DEFAULT_MIN_COUNT = 3;
1994
+ DEFAULT_PRACTICE_MIN_COUNT = 3;
1995
+ DEFAULT_MAX_PRACTICE_PER_RUN = 4;
1905
1996
  BASE_TARGET_SCORE = 1;
1906
1997
  BASE_SUPPORT_SCORE = 0.8;
1907
1998
  DISCOVERED_SUPPORT_SCORE = 12;
1999
+ BASE_PRACTICE_SCORE = 1;
1908
2000
  MAX_TARGET_MULTIPLIER = 8;
1909
2001
  MAX_SUPPORT_MULTIPLIER = 4;
1910
2002
  PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
@@ -2012,7 +2104,18 @@ var init_prescribed = __esm({
2012
2104
  courseId,
2013
2105
  emittedIds
2014
2106
  );
2015
- emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
2107
+ const practiceCards = this.buildPracticeCards({
2108
+ group,
2109
+ courseId,
2110
+ emittedIds,
2111
+ cardsByTag,
2112
+ hierarchyConfigs,
2113
+ userTagElo,
2114
+ userGlobalElo,
2115
+ activeIds,
2116
+ seenIds
2117
+ });
2118
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
2016
2119
  }
2017
2120
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
2018
2121
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
@@ -2040,6 +2143,7 @@ var init_prescribed = __esm({
2040
2143
  const surfacedByGroup = /* @__PURE__ */ new Map();
2041
2144
  for (const card of finalCards) {
2042
2145
  const prov = card.provenance[0];
2146
+ if (prov?.reason.includes("mode=practice")) continue;
2043
2147
  const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
2044
2148
  const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
2045
2149
  if (!groupId) continue;
@@ -2109,7 +2213,12 @@ var init_prescribed = __esm({
2109
2213
  enabled: raw.hierarchyWalk?.enabled !== false,
2110
2214
  maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
2111
2215
  },
2112
- retireOnEncounter: raw.retireOnEncounter !== false
2216
+ retireOnEncounter: raw.retireOnEncounter !== false,
2217
+ practiceTagPatterns: dedupe(
2218
+ Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
2219
+ ),
2220
+ practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
2221
+ maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
2113
2222
  })).filter((g) => g.targetCardIds.length > 0);
2114
2223
  return { groups };
2115
2224
  } catch {
@@ -2332,6 +2441,92 @@ var init_prescribed = __esm({
2332
2441
  }
2333
2442
  return cards;
2334
2443
  }
2444
+ /**
2445
+ * Emit drill cards for *unlocked-but-under-practiced* skills.
2446
+ *
2447
+ * For each course tag matching the group's `practiceTagPatterns` that is both
2448
+ * unlocked (all hierarchy prerequisites met — i.e. the learner has been
2449
+ * introduced to it) and under-practiced (per-tag attempt count below
2450
+ * `practiceMinCount`), this resolves cards carrying that tag and emits them
2451
+ * into the candidate pool. It exists because global-ELO retrieval
2452
+ * systematically fails to fetch the (low-ELO) drill cards for a
2453
+ * freshly-introduced skill — putting them in the pool here lets the pipeline's
2454
+ * scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
2455
+ * this method's job; it only guarantees presence.
2456
+ *
2457
+ * Fully data-driven: the unlock relation comes from the hierarchy config and
2458
+ * practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
2459
+ */
2460
+ buildPracticeCards(args) {
2461
+ const {
2462
+ group,
2463
+ courseId,
2464
+ emittedIds,
2465
+ cardsByTag,
2466
+ hierarchyConfigs,
2467
+ userTagElo,
2468
+ userGlobalElo,
2469
+ activeIds,
2470
+ seenIds
2471
+ } = args;
2472
+ const patterns = group.practiceTagPatterns ?? [];
2473
+ if (patterns.length === 0) return [];
2474
+ const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
2475
+ const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
2476
+ const practiceTags = [...cardsByTag.keys()].filter(
2477
+ (tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
2478
+ );
2479
+ if (practiceTags.length === 0) return [];
2480
+ const practiceCardIds = this.findDiscoveredSupportCards({
2481
+ supportTags: practiceTags,
2482
+ cardsByTag,
2483
+ activeIds,
2484
+ seenIds,
2485
+ excludedIds: emittedIds,
2486
+ limit: maxPractice
2487
+ });
2488
+ if (practiceCardIds.length === 0) return [];
2489
+ logger.info(
2490
+ `[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
2491
+ );
2492
+ const cards = [];
2493
+ for (const cardId of practiceCardIds) {
2494
+ emittedIds.add(cardId);
2495
+ cards.push({
2496
+ cardId,
2497
+ courseId,
2498
+ score: BASE_PRACTICE_SCORE,
2499
+ provenance: [
2500
+ {
2501
+ strategy: "prescribed",
2502
+ strategyName: this.strategyName || this.name,
2503
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2504
+ action: "generated",
2505
+ score: BASE_PRACTICE_SCORE,
2506
+ reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
2507
+ }
2508
+ ]
2509
+ });
2510
+ }
2511
+ return cards;
2512
+ }
2513
+ /**
2514
+ * True for a skill that was *gated and is now reached*: it has at least one
2515
+ * declared hierarchy prerequisite set, and every set is fully satisfied by the
2516
+ * learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
2517
+ * — an ungated tag was never "introduced" in the curricular sense, so it isn't
2518
+ * a post-intro drill target (e.g. whole-word spelling tags that share the
2519
+ * `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
2520
+ * ELO retrieval. This is the precise population the retrieval gap strands:
2521
+ * just-unlocked, low-ELO skills.
2522
+ */
2523
+ isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
2524
+ const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
2525
+ if (prereqSets.length === 0) return false;
2526
+ return prereqSets.every(
2527
+ (prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
2528
+ );
2529
+ }
2335
2530
  findSupportCardsByTags(group, tagsByCard, supportTags) {
2336
2531
  if (supportTags.length === 0) {
2337
2532
  return [];
@@ -4126,7 +4321,7 @@ function logResultCards(cards) {
4126
4321
  for (let i = 0; i < cards.length; i++) {
4127
4322
  const c = cards[i];
4128
4323
  const tags = c.tags?.slice(0, 3).join(", ") || "";
4129
- const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
4324
+ 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) => {
4130
4325
  const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
4131
4326
  return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
4132
4327
  }).join(" | ");
@@ -4157,6 +4352,7 @@ var init_Pipeline = __esm({
4157
4352
  init_logger();
4158
4353
  init_orchestration();
4159
4354
  init_PipelineDebugger();
4355
+ init_diversityRerank();
4160
4356
  VERBOSE_RESULTS = true;
4161
4357
  Pipeline = class extends ContentNavigator {
4162
4358
  generator;
@@ -4330,6 +4526,7 @@ var init_Pipeline = __esm({
4330
4526
  this._ephemeralHints = null;
4331
4527
  cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
4332
4528
  }
4529
+ cards = diversityRerank(cards);
4333
4530
  cards.sort((a, b) => b.score - a.score);
4334
4531
  const tFilter = performance.now();
4335
4532
  const result = cards.slice(0, limit);
@@ -4898,6 +5095,7 @@ var init_3 = __esm({
4898
5095
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
4899
5096
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
4900
5097
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
5098
+ "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
4901
5099
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
4902
5100
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
4903
5101
  "./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
@@ -4923,9 +5121,12 @@ var init_3 = __esm({
4923
5121
  var navigators_exports = {};
4924
5122
  __export(navigators_exports, {
4925
5123
  ContentNavigator: () => ContentNavigator,
5124
+ DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
5125
+ DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
4926
5126
  NavigatorRole: () => NavigatorRole,
4927
5127
  NavigatorRoles: () => NavigatorRoles,
4928
5128
  Navigators: () => Navigators,
5129
+ diversityRerank: () => diversityRerank,
4929
5130
  getCardOrigin: () => getCardOrigin,
4930
5131
  getRegisteredNavigator: () => getRegisteredNavigator,
4931
5132
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
@@ -5009,6 +5210,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
5009
5210
  var init_navigators = __esm({
5010
5211
  "src/core/navigators/index.ts"() {
5011
5212
  "use strict";
5213
+ init_diversityRerank();
5012
5214
  init_PipelineDebugger();
5013
5215
  init_logger();
5014
5216
  init_();
@@ -8032,6 +8234,8 @@ var init_core = __esm({
8032
8234
  init_core();
8033
8235
  export {
8034
8236
  ContentNavigator,
8237
+ DIVERSITY_FLOOR,
8238
+ DIVERSITY_STRENGTH,
8035
8239
  DocType,
8036
8240
  DocTypePrefixes,
8037
8241
  GuestUsername,
@@ -8048,6 +8252,7 @@ export {
8048
8252
  computeSpread,
8049
8253
  computeStrategyGradient,
8050
8254
  createOrchestrationContext,
8255
+ diversityRerank,
8051
8256
  docIsDeleted,
8052
8257
  getCardHistoryID,
8053
8258
  getCardOrigin,