@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.
@@ -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, {
@@ -1838,7 +1927,7 @@ function shuffleInPlace(arr) {
1838
1927
  function pickTopByScore(cards, limit) {
1839
1928
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1840
1929
  }
1841
- 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;
1842
1931
  var init_prescribed = __esm({
1843
1932
  "src/core/navigators/generators/prescribed.ts"() {
1844
1933
  "use strict";
@@ -1849,9 +1938,12 @@ var init_prescribed = __esm({
1849
1938
  DEFAULT_MAX_SUPPORT_PER_RUN = 3;
1850
1939
  DEFAULT_HIERARCHY_DEPTH = 2;
1851
1940
  DEFAULT_MIN_COUNT = 3;
1941
+ DEFAULT_PRACTICE_MIN_COUNT = 3;
1942
+ DEFAULT_MAX_PRACTICE_PER_RUN = 4;
1852
1943
  BASE_TARGET_SCORE = 1;
1853
1944
  BASE_SUPPORT_SCORE = 0.8;
1854
1945
  DISCOVERED_SUPPORT_SCORE = 12;
1946
+ BASE_PRACTICE_SCORE = 1;
1855
1947
  MAX_TARGET_MULTIPLIER = 8;
1856
1948
  MAX_SUPPORT_MULTIPLIER = 4;
1857
1949
  PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
@@ -1959,7 +2051,18 @@ var init_prescribed = __esm({
1959
2051
  courseId,
1960
2052
  emittedIds
1961
2053
  );
1962
- 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);
1963
2066
  }
1964
2067
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1965
2068
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
@@ -1987,6 +2090,7 @@ var init_prescribed = __esm({
1987
2090
  const surfacedByGroup = /* @__PURE__ */ new Map();
1988
2091
  for (const card of finalCards) {
1989
2092
  const prov = card.provenance[0];
2093
+ if (prov?.reason.includes("mode=practice")) continue;
1990
2094
  const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
1991
2095
  const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
1992
2096
  if (!groupId) continue;
@@ -2056,7 +2160,12 @@ var init_prescribed = __esm({
2056
2160
  enabled: raw.hierarchyWalk?.enabled !== false,
2057
2161
  maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
2058
2162
  },
2059
- 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
2060
2169
  })).filter((g) => g.targetCardIds.length > 0);
2061
2170
  return { groups };
2062
2171
  } catch {
@@ -2279,6 +2388,92 @@ var init_prescribed = __esm({
2279
2388
  }
2280
2389
  return cards;
2281
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
+ }
2282
2477
  findSupportCardsByTags(group, tagsByCard, supportTags) {
2283
2478
  if (supportTags.length === 0) {
2284
2479
  return [];
@@ -3826,7 +4021,7 @@ function logResultCards(cards) {
3826
4021
  for (let i = 0; i < cards.length; i++) {
3827
4022
  const c = cards[i];
3828
4023
  const tags = c.tags?.slice(0, 3).join(", ") || "";
3829
- 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) => {
3830
4025
  const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
3831
4026
  return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
3832
4027
  }).join(" | ");
@@ -3858,6 +4053,7 @@ var init_Pipeline = __esm({
3858
4053
  init_logger();
3859
4054
  init_orchestration();
3860
4055
  init_PipelineDebugger();
4056
+ init_diversityRerank();
3861
4057
  VERBOSE_RESULTS = true;
3862
4058
  Pipeline = class extends ContentNavigator {
3863
4059
  generator;
@@ -4031,6 +4227,7 @@ var init_Pipeline = __esm({
4031
4227
  this._ephemeralHints = null;
4032
4228
  cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
4033
4229
  }
4230
+ cards = diversityRerank(cards);
4034
4231
  cards.sort((a, b) => b.score - a.score);
4035
4232
  const tFilter = performance.now();
4036
4233
  const result = cards.slice(0, limit);
@@ -4599,6 +4796,7 @@ var init_3 = __esm({
4599
4796
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
4600
4797
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
4601
4798
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
4799
+ "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
4602
4800
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
4603
4801
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
4604
4802
  "./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
@@ -4624,9 +4822,12 @@ var init_3 = __esm({
4624
4822
  var navigators_exports = {};
4625
4823
  __export(navigators_exports, {
4626
4824
  ContentNavigator: () => ContentNavigator,
4825
+ DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
4826
+ DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
4627
4827
  NavigatorRole: () => NavigatorRole,
4628
4828
  NavigatorRoles: () => NavigatorRoles,
4629
4829
  Navigators: () => Navigators,
4830
+ diversityRerank: () => diversityRerank,
4630
4831
  getCardOrigin: () => getCardOrigin,
4631
4832
  getRegisteredNavigator: () => getRegisteredNavigator,
4632
4833
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
@@ -4710,6 +4911,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
4710
4911
  var init_navigators = __esm({
4711
4912
  "src/core/navigators/index.ts"() {
4712
4913
  "use strict";
4914
+ init_diversityRerank();
4713
4915
  init_PipelineDebugger();
4714
4916
  init_logger();
4715
4917
  init_();