@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.
@@ -621,6 +621,95 @@ var init_courseLookupDB = __esm({
621
621
  }
622
622
  });
623
623
 
624
+ // src/core/navigators/diversityRerank.ts
625
+ var diversityRerank_exports = {};
626
+ __export(diversityRerank_exports, {
627
+ DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
628
+ DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
629
+ diversityRerank: () => diversityRerank
630
+ });
631
+ function diversityRerank(cards, opts = {}) {
632
+ const strength = opts.strength ?? DIVERSITY_STRENGTH;
633
+ const floor = opts.floor ?? DIVERSITY_FLOOR;
634
+ const n = cards.length;
635
+ if (n <= 1) return cards;
636
+ const df = /* @__PURE__ */ new Map();
637
+ for (const card of cards) {
638
+ for (const tag of card.tags ?? []) {
639
+ df.set(tag, (df.get(tag) ?? 0) + 1);
640
+ }
641
+ }
642
+ const idf = /* @__PURE__ */ new Map();
643
+ for (const [tag, freq] of df) {
644
+ idf.set(tag, Math.log(n / freq));
645
+ }
646
+ const remaining = [...cards];
647
+ const emittedCount = /* @__PURE__ */ new Map();
648
+ const out = [];
649
+ const repetitionLoad = (card) => {
650
+ let load = 0;
651
+ for (const tag of card.tags ?? []) {
652
+ const seen = emittedCount.get(tag);
653
+ if (seen) load += (idf.get(tag) ?? 0) * seen;
654
+ }
655
+ return load;
656
+ };
657
+ while (remaining.length > 0) {
658
+ let bestIdx = 0;
659
+ let bestValue = -Infinity;
660
+ let bestPenalty = 1;
661
+ let bestLoad = 0;
662
+ for (let i = 0; i < remaining.length; i++) {
663
+ const card = remaining[i];
664
+ const load = repetitionLoad(card);
665
+ const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
666
+ const value = card.score * penalty;
667
+ if (value > bestValue) {
668
+ bestValue = value;
669
+ bestIdx = i;
670
+ bestPenalty = penalty;
671
+ bestLoad = load;
672
+ }
673
+ }
674
+ const [picked] = remaining.splice(bestIdx, 1);
675
+ if (Number.isFinite(picked.score) && bestPenalty < 1) {
676
+ const newScore = picked.score * bestPenalty;
677
+ out.push({
678
+ ...picked,
679
+ score: newScore,
680
+ provenance: [
681
+ ...picked.provenance,
682
+ {
683
+ strategy: STRATEGY,
684
+ strategyId: STRATEGY_ID,
685
+ strategyName: STRATEGY_NAME,
686
+ action: "penalized",
687
+ score: newScore,
688
+ reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
689
+ }
690
+ ]
691
+ });
692
+ } else {
693
+ out.push(picked);
694
+ }
695
+ for (const tag of picked.tags ?? []) {
696
+ emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
697
+ }
698
+ }
699
+ return out;
700
+ }
701
+ var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
702
+ var init_diversityRerank = __esm({
703
+ "src/core/navigators/diversityRerank.ts"() {
704
+ "use strict";
705
+ DIVERSITY_STRENGTH = 0.6;
706
+ DIVERSITY_FLOOR = 0.3;
707
+ STRATEGY = "diversityRerank";
708
+ STRATEGY_ID = "DIVERSITY_RERANK";
709
+ STRATEGY_NAME = "Diversity Re-rank";
710
+ }
711
+ });
712
+
624
713
  // src/core/navigators/PipelineDebugger.ts
625
714
  var PipelineDebugger_exports = {};
626
715
  __export(PipelineDebugger_exports, {
@@ -1816,7 +1905,7 @@ function shuffleInPlace(arr) {
1816
1905
  function pickTopByScore(cards, limit) {
1817
1906
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1818
1907
  }
1819
- 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;
1908
+ 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;
1820
1909
  var init_prescribed = __esm({
1821
1910
  "src/core/navigators/generators/prescribed.ts"() {
1822
1911
  "use strict";
@@ -1827,9 +1916,12 @@ var init_prescribed = __esm({
1827
1916
  DEFAULT_MAX_SUPPORT_PER_RUN = 3;
1828
1917
  DEFAULT_HIERARCHY_DEPTH = 2;
1829
1918
  DEFAULT_MIN_COUNT = 3;
1919
+ DEFAULT_PRACTICE_MIN_COUNT = 3;
1920
+ DEFAULT_MAX_PRACTICE_PER_RUN = 4;
1830
1921
  BASE_TARGET_SCORE = 1;
1831
1922
  BASE_SUPPORT_SCORE = 0.8;
1832
1923
  DISCOVERED_SUPPORT_SCORE = 12;
1924
+ BASE_PRACTICE_SCORE = 1;
1833
1925
  MAX_TARGET_MULTIPLIER = 8;
1834
1926
  MAX_SUPPORT_MULTIPLIER = 4;
1835
1927
  PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
@@ -1937,7 +2029,18 @@ var init_prescribed = __esm({
1937
2029
  courseId,
1938
2030
  emittedIds
1939
2031
  );
1940
- emitted.push(...directCards, ...supportCards, ...discoveredSupportCards);
2032
+ const practiceCards = this.buildPracticeCards({
2033
+ group,
2034
+ courseId,
2035
+ emittedIds,
2036
+ cardsByTag,
2037
+ hierarchyConfigs,
2038
+ userTagElo,
2039
+ userGlobalElo,
2040
+ activeIds,
2041
+ seenIds
2042
+ });
2043
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
1941
2044
  }
1942
2045
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1943
2046
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
@@ -1965,6 +2068,7 @@ var init_prescribed = __esm({
1965
2068
  const surfacedByGroup = /* @__PURE__ */ new Map();
1966
2069
  for (const card of finalCards) {
1967
2070
  const prov = card.provenance[0];
2071
+ if (prov?.reason.includes("mode=practice")) continue;
1968
2072
  const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
1969
2073
  const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
1970
2074
  if (!groupId) continue;
@@ -2034,7 +2138,12 @@ var init_prescribed = __esm({
2034
2138
  enabled: raw.hierarchyWalk?.enabled !== false,
2035
2139
  maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
2036
2140
  },
2037
- retireOnEncounter: raw.retireOnEncounter !== false
2141
+ retireOnEncounter: raw.retireOnEncounter !== false,
2142
+ practiceTagPatterns: dedupe(
2143
+ Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
2144
+ ),
2145
+ practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
2146
+ maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
2038
2147
  })).filter((g) => g.targetCardIds.length > 0);
2039
2148
  return { groups };
2040
2149
  } catch {
@@ -2257,6 +2366,92 @@ var init_prescribed = __esm({
2257
2366
  }
2258
2367
  return cards;
2259
2368
  }
2369
+ /**
2370
+ * Emit drill cards for *unlocked-but-under-practiced* skills.
2371
+ *
2372
+ * For each course tag matching the group's `practiceTagPatterns` that is both
2373
+ * unlocked (all hierarchy prerequisites met — i.e. the learner has been
2374
+ * introduced to it) and under-practiced (per-tag attempt count below
2375
+ * `practiceMinCount`), this resolves cards carrying that tag and emits them
2376
+ * into the candidate pool. It exists because global-ELO retrieval
2377
+ * systematically fails to fetch the (low-ELO) drill cards for a
2378
+ * freshly-introduced skill — putting them in the pool here lets the pipeline's
2379
+ * scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
2380
+ * this method's job; it only guarantees presence.
2381
+ *
2382
+ * Fully data-driven: the unlock relation comes from the hierarchy config and
2383
+ * practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
2384
+ */
2385
+ buildPracticeCards(args) {
2386
+ const {
2387
+ group,
2388
+ courseId,
2389
+ emittedIds,
2390
+ cardsByTag,
2391
+ hierarchyConfigs,
2392
+ userTagElo,
2393
+ userGlobalElo,
2394
+ activeIds,
2395
+ seenIds
2396
+ } = args;
2397
+ const patterns = group.practiceTagPatterns ?? [];
2398
+ if (patterns.length === 0) return [];
2399
+ const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
2400
+ const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
2401
+ const practiceTags = [...cardsByTag.keys()].filter(
2402
+ (tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
2403
+ );
2404
+ if (practiceTags.length === 0) return [];
2405
+ const practiceCardIds = this.findDiscoveredSupportCards({
2406
+ supportTags: practiceTags,
2407
+ cardsByTag,
2408
+ activeIds,
2409
+ seenIds,
2410
+ excludedIds: emittedIds,
2411
+ limit: maxPractice
2412
+ });
2413
+ if (practiceCardIds.length === 0) return [];
2414
+ logger.info(
2415
+ `[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
2416
+ );
2417
+ const cards = [];
2418
+ for (const cardId of practiceCardIds) {
2419
+ emittedIds.add(cardId);
2420
+ cards.push({
2421
+ cardId,
2422
+ courseId,
2423
+ score: BASE_PRACTICE_SCORE,
2424
+ provenance: [
2425
+ {
2426
+ strategy: "prescribed",
2427
+ strategyName: this.strategyName || this.name,
2428
+ strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
2429
+ action: "generated",
2430
+ score: BASE_PRACTICE_SCORE,
2431
+ reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
2432
+ }
2433
+ ]
2434
+ });
2435
+ }
2436
+ return cards;
2437
+ }
2438
+ /**
2439
+ * True for a skill that was *gated and is now reached*: it has at least one
2440
+ * declared hierarchy prerequisite set, and every set is fully satisfied by the
2441
+ * learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
2442
+ * — an ungated tag was never "introduced" in the curricular sense, so it isn't
2443
+ * a post-intro drill target (e.g. whole-word spelling tags that share the
2444
+ * `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
2445
+ * ELO retrieval. This is the precise population the retrieval gap strands:
2446
+ * just-unlocked, low-ELO skills.
2447
+ */
2448
+ isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
2449
+ const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
2450
+ if (prereqSets.length === 0) return false;
2451
+ return prereqSets.every(
2452
+ (prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
2453
+ );
2454
+ }
2260
2455
  findSupportCardsByTags(group, tagsByCard, supportTags) {
2261
2456
  if (supportTags.length === 0) {
2262
2457
  return [];
@@ -3805,7 +4000,7 @@ function logResultCards(cards) {
3805
4000
  for (let i = 0; i < cards.length; i++) {
3806
4001
  const c = cards[i];
3807
4002
  const tags = c.tags?.slice(0, 3).join(", ") || "";
3808
- const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
4003
+ 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) => {
3809
4004
  const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
3810
4005
  return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
3811
4006
  }).join(" | ");
@@ -3836,6 +4031,7 @@ var init_Pipeline = __esm({
3836
4031
  init_logger();
3837
4032
  init_orchestration();
3838
4033
  init_PipelineDebugger();
4034
+ init_diversityRerank();
3839
4035
  VERBOSE_RESULTS = true;
3840
4036
  Pipeline = class extends ContentNavigator {
3841
4037
  generator;
@@ -4009,6 +4205,7 @@ var init_Pipeline = __esm({
4009
4205
  this._ephemeralHints = null;
4010
4206
  cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
4011
4207
  }
4208
+ cards = diversityRerank(cards);
4012
4209
  cards.sort((a, b) => b.score - a.score);
4013
4210
  const tFilter = performance.now();
4014
4211
  const result = cards.slice(0, limit);
@@ -4577,6 +4774,7 @@ var init_3 = __esm({
4577
4774
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
4578
4775
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
4579
4776
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
4777
+ "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
4580
4778
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
4581
4779
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
4582
4780
  "./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
@@ -4602,9 +4800,12 @@ var init_3 = __esm({
4602
4800
  var navigators_exports = {};
4603
4801
  __export(navigators_exports, {
4604
4802
  ContentNavigator: () => ContentNavigator,
4803
+ DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
4804
+ DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
4605
4805
  NavigatorRole: () => NavigatorRole,
4606
4806
  NavigatorRoles: () => NavigatorRoles,
4607
4807
  Navigators: () => Navigators,
4808
+ diversityRerank: () => diversityRerank,
4608
4809
  getCardOrigin: () => getCardOrigin,
4609
4810
  getRegisteredNavigator: () => getRegisteredNavigator,
4610
4811
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
@@ -4688,6 +4889,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
4688
4889
  var init_navigators = __esm({
4689
4890
  "src/core/navigators/index.ts"() {
4690
4891
  "use strict";
4892
+ init_diversityRerank();
4691
4893
  init_PipelineDebugger();
4692
4894
  init_logger();
4693
4895
  init_();