@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.
@@ -522,6 +522,95 @@ var init_courseLookupDB = __esm({
522
522
  }
523
523
  });
524
524
 
525
+ // src/core/navigators/diversityRerank.ts
526
+ var diversityRerank_exports = {};
527
+ __export(diversityRerank_exports, {
528
+ DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
529
+ DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
530
+ diversityRerank: () => diversityRerank
531
+ });
532
+ function diversityRerank(cards, opts = {}) {
533
+ const strength = opts.strength ?? DIVERSITY_STRENGTH;
534
+ const floor = opts.floor ?? DIVERSITY_FLOOR;
535
+ const n = cards.length;
536
+ if (n <= 1) return cards;
537
+ const df = /* @__PURE__ */ new Map();
538
+ for (const card of cards) {
539
+ for (const tag of card.tags ?? []) {
540
+ df.set(tag, (df.get(tag) ?? 0) + 1);
541
+ }
542
+ }
543
+ const idf = /* @__PURE__ */ new Map();
544
+ for (const [tag, freq] of df) {
545
+ idf.set(tag, Math.log(n / freq));
546
+ }
547
+ const remaining = [...cards];
548
+ const emittedCount = /* @__PURE__ */ new Map();
549
+ const out = [];
550
+ const repetitionLoad = (card) => {
551
+ let load = 0;
552
+ for (const tag of card.tags ?? []) {
553
+ const seen = emittedCount.get(tag);
554
+ if (seen) load += (idf.get(tag) ?? 0) * seen;
555
+ }
556
+ return load;
557
+ };
558
+ while (remaining.length > 0) {
559
+ let bestIdx = 0;
560
+ let bestValue = -Infinity;
561
+ let bestPenalty = 1;
562
+ let bestLoad = 0;
563
+ for (let i = 0; i < remaining.length; i++) {
564
+ const card = remaining[i];
565
+ const load = repetitionLoad(card);
566
+ const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
567
+ const value = card.score * penalty;
568
+ if (value > bestValue) {
569
+ bestValue = value;
570
+ bestIdx = i;
571
+ bestPenalty = penalty;
572
+ bestLoad = load;
573
+ }
574
+ }
575
+ const [picked] = remaining.splice(bestIdx, 1);
576
+ if (Number.isFinite(picked.score) && bestPenalty < 1) {
577
+ const newScore = picked.score * bestPenalty;
578
+ out.push({
579
+ ...picked,
580
+ score: newScore,
581
+ provenance: [
582
+ ...picked.provenance,
583
+ {
584
+ strategy: STRATEGY,
585
+ strategyId: STRATEGY_ID,
586
+ strategyName: STRATEGY_NAME,
587
+ action: "penalized",
588
+ score: newScore,
589
+ reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
590
+ }
591
+ ]
592
+ });
593
+ } else {
594
+ out.push(picked);
595
+ }
596
+ for (const tag of picked.tags ?? []) {
597
+ emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
598
+ }
599
+ }
600
+ return out;
601
+ }
602
+ var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
603
+ var init_diversityRerank = __esm({
604
+ "src/core/navigators/diversityRerank.ts"() {
605
+ "use strict";
606
+ DIVERSITY_STRENGTH = 0.6;
607
+ DIVERSITY_FLOOR = 0.3;
608
+ STRATEGY = "diversityRerank";
609
+ STRATEGY_ID = "DIVERSITY_RERANK";
610
+ STRATEGY_NAME = "Diversity Re-rank";
611
+ }
612
+ });
613
+
525
614
  // src/core/navigators/PipelineDebugger.ts
526
615
  var PipelineDebugger_exports = {};
527
616
  __export(PipelineDebugger_exports, {
@@ -1589,13 +1678,14 @@ var elo_exports = {};
1589
1678
  __export(elo_exports, {
1590
1679
  default: () => ELONavigator
1591
1680
  });
1592
- var import_common5, ELONavigator;
1681
+ var import_common5, ELO_RELEVANCE_SIGMA, ELONavigator;
1593
1682
  var init_elo = __esm({
1594
1683
  "src/core/navigators/generators/elo.ts"() {
1595
1684
  "use strict";
1596
1685
  init_navigators();
1597
1686
  import_common5 = require("@vue-skuilder/common");
1598
1687
  init_logger();
1688
+ ELO_RELEVANCE_SIGMA = 300;
1599
1689
  ELONavigator = class extends ContentNavigator {
1600
1690
  /** Human-readable name for CardGenerator interface */
1601
1691
  name;
@@ -1635,8 +1725,8 @@ var init_elo = __esm({
1635
1725
  const scored = newCards.map((c) => {
1636
1726
  const cardElo = c.elo ?? 1e3;
1637
1727
  const distance = Math.abs(cardElo - userGlobalElo);
1638
- const rawScore = Math.max(0, 1 - distance / 500);
1639
- const samplingKey = rawScore > 0 ? Math.random() ** (1 / rawScore) : 0;
1728
+ const relevance = Math.exp(-((distance / ELO_RELEVANCE_SIGMA) ** 2));
1729
+ const samplingKey = relevance * (0.5 + 0.5 * Math.random());
1640
1730
  return {
1641
1731
  cardId: c.cardID,
1642
1732
  courseId: c.courseID,
@@ -1648,7 +1738,7 @@ var init_elo = __esm({
1648
1738
  strategyId: this.strategyId || "NAVIGATION_STRATEGY-ELO-default",
1649
1739
  action: "generated",
1650
1740
  score: samplingKey,
1651
- reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), raw ${rawScore.toFixed(3)}, key ${samplingKey.toFixed(3)}`
1741
+ reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), relevance ${relevance.toFixed(3)}, key ${samplingKey.toFixed(3)}`
1652
1742
  }
1653
1743
  ]
1654
1744
  };
@@ -1716,7 +1806,7 @@ function shuffleInPlace(arr) {
1716
1806
  function pickTopByScore(cards, limit) {
1717
1807
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1718
1808
  }
1719
- 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;
1809
+ 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;
1720
1810
  var init_prescribed = __esm({
1721
1811
  "src/core/navigators/generators/prescribed.ts"() {
1722
1812
  "use strict";
@@ -1727,9 +1817,12 @@ var init_prescribed = __esm({
1727
1817
  DEFAULT_MAX_SUPPORT_PER_RUN = 3;
1728
1818
  DEFAULT_HIERARCHY_DEPTH = 2;
1729
1819
  DEFAULT_MIN_COUNT = 3;
1820
+ DEFAULT_PRACTICE_MIN_COUNT = 3;
1821
+ DEFAULT_MAX_PRACTICE_PER_RUN = 4;
1730
1822
  BASE_TARGET_SCORE = 1;
1731
1823
  BASE_SUPPORT_SCORE = 0.8;
1732
1824
  DISCOVERED_SUPPORT_SCORE = 12;
1825
+ BASE_PRACTICE_SCORE = 1;
1733
1826
  MAX_TARGET_MULTIPLIER = 8;
1734
1827
  MAX_SUPPORT_MULTIPLIER = 4;
1735
1828
  PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
@@ -1837,7 +1930,18 @@ var init_prescribed = __esm({
1837
1930
  courseId,
1838
1931
  emittedIds
1839
1932
  );
1840
- emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
1933
+ const practiceCards = this.buildPracticeCards({
1934
+ group,
1935
+ courseId,
1936
+ emittedIds,
1937
+ cardsByTag,
1938
+ hierarchyConfigs,
1939
+ userTagElo,
1940
+ userGlobalElo,
1941
+ activeIds,
1942
+ seenIds
1943
+ });
1944
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
1841
1945
  }
1842
1946
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1843
1947
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
@@ -1865,6 +1969,7 @@ var init_prescribed = __esm({
1865
1969
  const surfacedByGroup = /* @__PURE__ */ new Map();
1866
1970
  for (const card of finalCards) {
1867
1971
  const prov = card.provenance[0];
1972
+ if (prov?.reason.includes("mode=practice")) continue;
1868
1973
  const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
1869
1974
  const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
1870
1975
  if (!groupId) continue;
@@ -1934,7 +2039,12 @@ var init_prescribed = __esm({
1934
2039
  enabled: raw.hierarchyWalk?.enabled !== false,
1935
2040
  maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
1936
2041
  },
1937
- retireOnEncounter: raw.retireOnEncounter !== false
2042
+ retireOnEncounter: raw.retireOnEncounter !== false,
2043
+ practiceTagPatterns: dedupe(
2044
+ Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
2045
+ ),
2046
+ practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
2047
+ maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
1938
2048
  })).filter((g) => g.targetCardIds.length > 0);
1939
2049
  return { groups };
1940
2050
  } catch {
@@ -2157,6 +2267,92 @@ var init_prescribed = __esm({
2157
2267
  }
2158
2268
  return cards;
2159
2269
  }
2270
+ /**
2271
+ * Emit drill cards for *unlocked-but-under-practiced* skills.
2272
+ *
2273
+ * For each course tag matching the group's `practiceTagPatterns` that is both
2274
+ * unlocked (all hierarchy prerequisites met — i.e. the learner has been
2275
+ * introduced to it) and under-practiced (per-tag attempt count below
2276
+ * `practiceMinCount`), this resolves cards carrying that tag and emits them
2277
+ * into the candidate pool. It exists because global-ELO retrieval
2278
+ * systematically fails to fetch the (low-ELO) drill cards for a
2279
+ * freshly-introduced skill — putting them in the pool here lets the pipeline's
2280
+ * scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
2281
+ * this method's job; it only guarantees presence.
2282
+ *
2283
+ * Fully data-driven: the unlock relation comes from the hierarchy config and
2284
+ * practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
2285
+ */
2286
+ buildPracticeCards(args) {
2287
+ const {
2288
+ group,
2289
+ courseId,
2290
+ emittedIds,
2291
+ cardsByTag,
2292
+ hierarchyConfigs,
2293
+ userTagElo,
2294
+ userGlobalElo,
2295
+ activeIds,
2296
+ seenIds
2297
+ } = args;
2298
+ const patterns = group.practiceTagPatterns ?? [];
2299
+ if (patterns.length === 0) return [];
2300
+ const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
2301
+ const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
2302
+ const practiceTags = [...cardsByTag.keys()].filter(
2303
+ (tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
2304
+ );
2305
+ if (practiceTags.length === 0) return [];
2306
+ const practiceCardIds = this.findDiscoveredSupportCards({
2307
+ supportTags: practiceTags,
2308
+ cardsByTag,
2309
+ activeIds,
2310
+ seenIds,
2311
+ excludedIds: emittedIds,
2312
+ limit: maxPractice
2313
+ });
2314
+ if (practiceCardIds.length === 0) return [];
2315
+ logger.info(
2316
+ `[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
2317
+ );
2318
+ const cards = [];
2319
+ for (const cardId of practiceCardIds) {
2320
+ emittedIds.add(cardId);
2321
+ cards.push({
2322
+ cardId,
2323
+ courseId,
2324
+ score: BASE_PRACTICE_SCORE,
2325
+ provenance: [
2326
+ {
2327
+ strategy: "prescribed",
2328
+ strategyName: this.strategyName || this.name,
2329
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2330
+ action: "generated",
2331
+ score: BASE_PRACTICE_SCORE,
2332
+ reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
2333
+ }
2334
+ ]
2335
+ });
2336
+ }
2337
+ return cards;
2338
+ }
2339
+ /**
2340
+ * True for a skill that was *gated and is now reached*: it has at least one
2341
+ * declared hierarchy prerequisite set, and every set is fully satisfied by the
2342
+ * learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
2343
+ * — an ungated tag was never "introduced" in the curricular sense, so it isn't
2344
+ * a post-intro drill target (e.g. whole-word spelling tags that share the
2345
+ * `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
2346
+ * ELO retrieval. This is the precise population the retrieval gap strands:
2347
+ * just-unlocked, low-ELO skills.
2348
+ */
2349
+ isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
2350
+ const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
2351
+ if (prereqSets.length === 0) return false;
2352
+ return prereqSets.every(
2353
+ (prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
2354
+ );
2355
+ }
2160
2356
  findSupportCardsByTags(group, tagsByCard, supportTags) {
2161
2357
  if (supportTags.length === 0) {
2162
2358
  return [];
@@ -3704,7 +3900,7 @@ function logResultCards(cards) {
3704
3900
  for (let i = 0; i < cards.length; i++) {
3705
3901
  const c = cards[i];
3706
3902
  const tags = c.tags?.slice(0, 3).join(", ") || "";
3707
- const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
3903
+ 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) => {
3708
3904
  const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
3709
3905
  return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
3710
3906
  }).join(" | ");
@@ -3736,6 +3932,7 @@ var init_Pipeline = __esm({
3736
3932
  init_logger();
3737
3933
  init_orchestration();
3738
3934
  init_PipelineDebugger();
3935
+ init_diversityRerank();
3739
3936
  VERBOSE_RESULTS = true;
3740
3937
  Pipeline = class extends ContentNavigator {
3741
3938
  generator;
@@ -3909,6 +4106,7 @@ var init_Pipeline = __esm({
3909
4106
  this._ephemeralHints = null;
3910
4107
  cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
3911
4108
  }
4109
+ cards = diversityRerank(cards);
3912
4110
  cards.sort((a, b) => b.score - a.score);
3913
4111
  const tFilter = performance.now();
3914
4112
  const result = cards.slice(0, limit);
@@ -4477,6 +4675,7 @@ var init_3 = __esm({
4477
4675
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
4478
4676
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
4479
4677
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
4678
+ "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
4480
4679
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
4481
4680
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
4482
4681
  "./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
@@ -4502,9 +4701,12 @@ var init_3 = __esm({
4502
4701
  var navigators_exports = {};
4503
4702
  __export(navigators_exports, {
4504
4703
  ContentNavigator: () => ContentNavigator,
4704
+ DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
4705
+ DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
4505
4706
  NavigatorRole: () => NavigatorRole,
4506
4707
  NavigatorRoles: () => NavigatorRoles,
4507
4708
  Navigators: () => Navigators,
4709
+ diversityRerank: () => diversityRerank,
4508
4710
  getCardOrigin: () => getCardOrigin,
4509
4711
  getRegisteredNavigator: () => getRegisteredNavigator,
4510
4712
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
@@ -4588,6 +4790,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
4588
4790
  var init_navigators = __esm({
4589
4791
  "src/core/navigators/index.ts"() {
4590
4792
  "use strict";
4793
+ init_diversityRerank();
4591
4794
  init_PipelineDebugger();
4592
4795
  init_logger();
4593
4796
  init_();