@vue-skuilder/db 0.2.4 → 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.
@@ -643,6 +643,95 @@ var init_courseLookupDB = __esm({
643
643
  }
644
644
  });
645
645
 
646
+ // src/core/navigators/diversityRerank.ts
647
+ var diversityRerank_exports = {};
648
+ __export(diversityRerank_exports, {
649
+ DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
650
+ DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
651
+ diversityRerank: () => diversityRerank
652
+ });
653
+ function diversityRerank(cards, opts = {}) {
654
+ const strength = opts.strength ?? DIVERSITY_STRENGTH;
655
+ const floor = opts.floor ?? DIVERSITY_FLOOR;
656
+ const n = cards.length;
657
+ if (n <= 1) return cards;
658
+ const df = /* @__PURE__ */ new Map();
659
+ for (const card of cards) {
660
+ for (const tag of card.tags ?? []) {
661
+ df.set(tag, (df.get(tag) ?? 0) + 1);
662
+ }
663
+ }
664
+ const idf = /* @__PURE__ */ new Map();
665
+ for (const [tag, freq] of df) {
666
+ idf.set(tag, Math.log(n / freq));
667
+ }
668
+ const remaining = [...cards];
669
+ const emittedCount = /* @__PURE__ */ new Map();
670
+ const out = [];
671
+ const repetitionLoad = (card) => {
672
+ let load = 0;
673
+ for (const tag of card.tags ?? []) {
674
+ const seen = emittedCount.get(tag);
675
+ if (seen) load += (idf.get(tag) ?? 0) * seen;
676
+ }
677
+ return load;
678
+ };
679
+ while (remaining.length > 0) {
680
+ let bestIdx = 0;
681
+ let bestValue = -Infinity;
682
+ let bestPenalty = 1;
683
+ let bestLoad = 0;
684
+ for (let i = 0; i < remaining.length; i++) {
685
+ const card = remaining[i];
686
+ const load = repetitionLoad(card);
687
+ const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
688
+ const value = card.score * penalty;
689
+ if (value > bestValue) {
690
+ bestValue = value;
691
+ bestIdx = i;
692
+ bestPenalty = penalty;
693
+ bestLoad = load;
694
+ }
695
+ }
696
+ const [picked] = remaining.splice(bestIdx, 1);
697
+ if (Number.isFinite(picked.score) && bestPenalty < 1) {
698
+ const newScore = picked.score * bestPenalty;
699
+ out.push({
700
+ ...picked,
701
+ score: newScore,
702
+ provenance: [
703
+ ...picked.provenance,
704
+ {
705
+ strategy: STRATEGY,
706
+ strategyId: STRATEGY_ID,
707
+ strategyName: STRATEGY_NAME,
708
+ action: "penalized",
709
+ score: newScore,
710
+ reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
711
+ }
712
+ ]
713
+ });
714
+ } else {
715
+ out.push(picked);
716
+ }
717
+ for (const tag of picked.tags ?? []) {
718
+ emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
719
+ }
720
+ }
721
+ return out;
722
+ }
723
+ var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
724
+ var init_diversityRerank = __esm({
725
+ "src/core/navigators/diversityRerank.ts"() {
726
+ "use strict";
727
+ DIVERSITY_STRENGTH = 0.6;
728
+ DIVERSITY_FLOOR = 0.3;
729
+ STRATEGY = "diversityRerank";
730
+ STRATEGY_ID = "DIVERSITY_RERANK";
731
+ STRATEGY_NAME = "Diversity Re-rank";
732
+ }
733
+ });
734
+
646
735
  // src/core/navigators/PipelineDebugger.ts
647
736
  var PipelineDebugger_exports = {};
648
737
  __export(PipelineDebugger_exports, {
@@ -1710,13 +1799,14 @@ var elo_exports = {};
1710
1799
  __export(elo_exports, {
1711
1800
  default: () => ELONavigator
1712
1801
  });
1713
- var import_common5, ELONavigator;
1802
+ var import_common5, ELO_RELEVANCE_SIGMA, ELONavigator;
1714
1803
  var init_elo = __esm({
1715
1804
  "src/core/navigators/generators/elo.ts"() {
1716
1805
  "use strict";
1717
1806
  init_navigators();
1718
1807
  import_common5 = require("@vue-skuilder/common");
1719
1808
  init_logger();
1809
+ ELO_RELEVANCE_SIGMA = 300;
1720
1810
  ELONavigator = class extends ContentNavigator {
1721
1811
  /** Human-readable name for CardGenerator interface */
1722
1812
  name;
@@ -1756,8 +1846,8 @@ var init_elo = __esm({
1756
1846
  const scored = newCards.map((c) => {
1757
1847
  const cardElo = c.elo ?? 1e3;
1758
1848
  const distance = Math.abs(cardElo - userGlobalElo);
1759
- const rawScore = Math.max(0, 1 - distance / 500);
1760
- const samplingKey = rawScore > 0 ? Math.random() ** (1 / rawScore) : 0;
1849
+ const relevance = Math.exp(-((distance / ELO_RELEVANCE_SIGMA) ** 2));
1850
+ const samplingKey = relevance * (0.5 + 0.5 * Math.random());
1761
1851
  return {
1762
1852
  cardId: c.cardID,
1763
1853
  courseId: c.courseID,
@@ -1769,7 +1859,7 @@ var init_elo = __esm({
1769
1859
  strategyId: this.strategyId || "NAVIGATION_STRATEGY-ELO-default",
1770
1860
  action: "generated",
1771
1861
  score: samplingKey,
1772
- reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), raw ${rawScore.toFixed(3)}, key ${samplingKey.toFixed(3)}`
1862
+ reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), relevance ${relevance.toFixed(3)}, key ${samplingKey.toFixed(3)}`
1773
1863
  }
1774
1864
  ]
1775
1865
  };
@@ -1837,7 +1927,7 @@ function shuffleInPlace(arr) {
1837
1927
  function pickTopByScore(cards, limit) {
1838
1928
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1839
1929
  }
1840
- 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;
1930
+ 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;
1841
1931
  var init_prescribed = __esm({
1842
1932
  "src/core/navigators/generators/prescribed.ts"() {
1843
1933
  "use strict";
@@ -1848,9 +1938,12 @@ var init_prescribed = __esm({
1848
1938
  DEFAULT_MAX_SUPPORT_PER_RUN = 3;
1849
1939
  DEFAULT_HIERARCHY_DEPTH = 2;
1850
1940
  DEFAULT_MIN_COUNT = 3;
1941
+ DEFAULT_PRACTICE_MIN_COUNT = 3;
1942
+ DEFAULT_MAX_PRACTICE_PER_RUN = 4;
1851
1943
  BASE_TARGET_SCORE = 1;
1852
1944
  BASE_SUPPORT_SCORE = 0.8;
1853
1945
  DISCOVERED_SUPPORT_SCORE = 12;
1946
+ BASE_PRACTICE_SCORE = 1;
1854
1947
  MAX_TARGET_MULTIPLIER = 8;
1855
1948
  MAX_SUPPORT_MULTIPLIER = 4;
1856
1949
  PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
@@ -1958,7 +2051,18 @@ var init_prescribed = __esm({
1958
2051
  courseId,
1959
2052
  emittedIds
1960
2053
  );
1961
- emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
2054
+ const practiceCards = this.buildPracticeCards({
2055
+ group,
2056
+ courseId,
2057
+ emittedIds,
2058
+ cardsByTag,
2059
+ hierarchyConfigs,
2060
+ userTagElo,
2061
+ userGlobalElo,
2062
+ activeIds,
2063
+ seenIds
2064
+ });
2065
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
1962
2066
  }
1963
2067
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1964
2068
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
@@ -1986,6 +2090,7 @@ var init_prescribed = __esm({
1986
2090
  const surfacedByGroup = /* @__PURE__ */ new Map();
1987
2091
  for (const card of finalCards) {
1988
2092
  const prov = card.provenance[0];
2093
+ if (prov?.reason.includes("mode=practice")) continue;
1989
2094
  const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
1990
2095
  const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
1991
2096
  if (!groupId) continue;
@@ -2055,7 +2160,12 @@ var init_prescribed = __esm({
2055
2160
  enabled: raw.hierarchyWalk?.enabled !== false,
2056
2161
  maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
2057
2162
  },
2058
- retireOnEncounter: raw.retireOnEncounter !== false
2163
+ retireOnEncounter: raw.retireOnEncounter !== false,
2164
+ practiceTagPatterns: dedupe(
2165
+ Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
2166
+ ),
2167
+ practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
2168
+ maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
2059
2169
  })).filter((g) => g.targetCardIds.length > 0);
2060
2170
  return { groups };
2061
2171
  } catch {
@@ -2278,6 +2388,92 @@ var init_prescribed = __esm({
2278
2388
  }
2279
2389
  return cards;
2280
2390
  }
2391
+ /**
2392
+ * Emit drill cards for *unlocked-but-under-practiced* skills.
2393
+ *
2394
+ * For each course tag matching the group's `practiceTagPatterns` that is both
2395
+ * unlocked (all hierarchy prerequisites met — i.e. the learner has been
2396
+ * introduced to it) and under-practiced (per-tag attempt count below
2397
+ * `practiceMinCount`), this resolves cards carrying that tag and emits them
2398
+ * into the candidate pool. It exists because global-ELO retrieval
2399
+ * systematically fails to fetch the (low-ELO) drill cards for a
2400
+ * freshly-introduced skill — putting them in the pool here lets the pipeline's
2401
+ * scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
2402
+ * this method's job; it only guarantees presence.
2403
+ *
2404
+ * Fully data-driven: the unlock relation comes from the hierarchy config and
2405
+ * practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
2406
+ */
2407
+ buildPracticeCards(args) {
2408
+ const {
2409
+ group,
2410
+ courseId,
2411
+ emittedIds,
2412
+ cardsByTag,
2413
+ hierarchyConfigs,
2414
+ userTagElo,
2415
+ userGlobalElo,
2416
+ activeIds,
2417
+ seenIds
2418
+ } = args;
2419
+ const patterns = group.practiceTagPatterns ?? [];
2420
+ if (patterns.length === 0) return [];
2421
+ const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
2422
+ const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
2423
+ const practiceTags = [...cardsByTag.keys()].filter(
2424
+ (tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
2425
+ );
2426
+ if (practiceTags.length === 0) return [];
2427
+ const practiceCardIds = this.findDiscoveredSupportCards({
2428
+ supportTags: practiceTags,
2429
+ cardsByTag,
2430
+ activeIds,
2431
+ seenIds,
2432
+ excludedIds: emittedIds,
2433
+ limit: maxPractice
2434
+ });
2435
+ if (practiceCardIds.length === 0) return [];
2436
+ logger.info(
2437
+ `[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
2438
+ );
2439
+ const cards = [];
2440
+ for (const cardId of practiceCardIds) {
2441
+ emittedIds.add(cardId);
2442
+ cards.push({
2443
+ cardId,
2444
+ courseId,
2445
+ score: BASE_PRACTICE_SCORE,
2446
+ provenance: [
2447
+ {
2448
+ strategy: "prescribed",
2449
+ strategyName: this.strategyName || this.name,
2450
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2451
+ action: "generated",
2452
+ score: BASE_PRACTICE_SCORE,
2453
+ reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
2454
+ }
2455
+ ]
2456
+ });
2457
+ }
2458
+ return cards;
2459
+ }
2460
+ /**
2461
+ * True for a skill that was *gated and is now reached*: it has at least one
2462
+ * declared hierarchy prerequisite set, and every set is fully satisfied by the
2463
+ * learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
2464
+ * — an ungated tag was never "introduced" in the curricular sense, so it isn't
2465
+ * a post-intro drill target (e.g. whole-word spelling tags that share the
2466
+ * `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
2467
+ * ELO retrieval. This is the precise population the retrieval gap strands:
2468
+ * just-unlocked, low-ELO skills.
2469
+ */
2470
+ isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
2471
+ const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
2472
+ if (prereqSets.length === 0) return false;
2473
+ return prereqSets.every(
2474
+ (prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
2475
+ );
2476
+ }
2281
2477
  findSupportCardsByTags(group, tagsByCard, supportTags) {
2282
2478
  if (supportTags.length === 0) {
2283
2479
  return [];
@@ -3825,7 +4021,7 @@ function logResultCards(cards) {
3825
4021
  for (let i = 0; i < cards.length; i++) {
3826
4022
  const c = cards[i];
3827
4023
  const tags = c.tags?.slice(0, 3).join(", ") || "";
3828
- const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
4024
+ 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) => {
3829
4025
  const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
3830
4026
  return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
3831
4027
  }).join(" | ");
@@ -3857,6 +4053,7 @@ var init_Pipeline = __esm({
3857
4053
  init_logger();
3858
4054
  init_orchestration();
3859
4055
  init_PipelineDebugger();
4056
+ init_diversityRerank();
3860
4057
  VERBOSE_RESULTS = true;
3861
4058
  Pipeline = class extends ContentNavigator {
3862
4059
  generator;
@@ -4030,6 +4227,7 @@ var init_Pipeline = __esm({
4030
4227
  this._ephemeralHints = null;
4031
4228
  cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
4032
4229
  }
4230
+ cards = diversityRerank(cards);
4033
4231
  cards.sort((a, b) => b.score - a.score);
4034
4232
  const tFilter = performance.now();
4035
4233
  const result = cards.slice(0, limit);
@@ -4598,6 +4796,7 @@ var init_3 = __esm({
4598
4796
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
4599
4797
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
4600
4798
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
4799
+ "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
4601
4800
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
4602
4801
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
4603
4802
  "./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
@@ -4623,9 +4822,12 @@ var init_3 = __esm({
4623
4822
  var navigators_exports = {};
4624
4823
  __export(navigators_exports, {
4625
4824
  ContentNavigator: () => ContentNavigator,
4825
+ DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
4826
+ DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
4626
4827
  NavigatorRole: () => NavigatorRole,
4627
4828
  NavigatorRoles: () => NavigatorRoles,
4628
4829
  Navigators: () => Navigators,
4830
+ diversityRerank: () => diversityRerank,
4629
4831
  getCardOrigin: () => getCardOrigin,
4630
4832
  getRegisteredNavigator: () => getRegisteredNavigator,
4631
4833
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
@@ -4709,6 +4911,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
4709
4911
  var init_navigators = __esm({
4710
4912
  "src/core/navigators/index.ts"() {
4711
4913
  "use strict";
4914
+ init_diversityRerank();
4712
4915
  init_PipelineDebugger();
4713
4916
  init_logger();
4714
4917
  init_();