@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.
@@ -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, {
@@ -1717,7 +1806,7 @@ function shuffleInPlace(arr) {
1717
1806
  function pickTopByScore(cards, limit) {
1718
1807
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1719
1808
  }
1720
- 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;
1721
1810
  var init_prescribed = __esm({
1722
1811
  "src/core/navigators/generators/prescribed.ts"() {
1723
1812
  "use strict";
@@ -1728,9 +1817,12 @@ var init_prescribed = __esm({
1728
1817
  DEFAULT_MAX_SUPPORT_PER_RUN = 3;
1729
1818
  DEFAULT_HIERARCHY_DEPTH = 2;
1730
1819
  DEFAULT_MIN_COUNT = 3;
1820
+ DEFAULT_PRACTICE_MIN_COUNT = 3;
1821
+ DEFAULT_MAX_PRACTICE_PER_RUN = 4;
1731
1822
  BASE_TARGET_SCORE = 1;
1732
1823
  BASE_SUPPORT_SCORE = 0.8;
1733
1824
  DISCOVERED_SUPPORT_SCORE = 12;
1825
+ BASE_PRACTICE_SCORE = 1;
1734
1826
  MAX_TARGET_MULTIPLIER = 8;
1735
1827
  MAX_SUPPORT_MULTIPLIER = 4;
1736
1828
  PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
@@ -1838,7 +1930,18 @@ var init_prescribed = __esm({
1838
1930
  courseId,
1839
1931
  emittedIds
1840
1932
  );
1841
- 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);
1842
1945
  }
1843
1946
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
1844
1947
  const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
@@ -1866,6 +1969,7 @@ var init_prescribed = __esm({
1866
1969
  const surfacedByGroup = /* @__PURE__ */ new Map();
1867
1970
  for (const card of finalCards) {
1868
1971
  const prov = card.provenance[0];
1972
+ if (prov?.reason.includes("mode=practice")) continue;
1869
1973
  const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
1870
1974
  const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
1871
1975
  if (!groupId) continue;
@@ -1935,7 +2039,12 @@ var init_prescribed = __esm({
1935
2039
  enabled: raw.hierarchyWalk?.enabled !== false,
1936
2040
  maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
1937
2041
  },
1938
- 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
1939
2048
  })).filter((g) => g.targetCardIds.length > 0);
1940
2049
  return { groups };
1941
2050
  } catch {
@@ -2158,6 +2267,92 @@ var init_prescribed = __esm({
2158
2267
  }
2159
2268
  return cards;
2160
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
+ }
2161
2356
  findSupportCardsByTags(group, tagsByCard, supportTags) {
2162
2357
  if (supportTags.length === 0) {
2163
2358
  return [];
@@ -3705,7 +3900,7 @@ function logResultCards(cards) {
3705
3900
  for (let i = 0; i < cards.length; i++) {
3706
3901
  const c = cards[i];
3707
3902
  const tags = c.tags?.slice(0, 3).join(", ") || "";
3708
- 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) => {
3709
3904
  const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
3710
3905
  return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
3711
3906
  }).join(" | ");
@@ -3737,6 +3932,7 @@ var init_Pipeline = __esm({
3737
3932
  init_logger();
3738
3933
  init_orchestration();
3739
3934
  init_PipelineDebugger();
3935
+ init_diversityRerank();
3740
3936
  VERBOSE_RESULTS = true;
3741
3937
  Pipeline = class extends ContentNavigator {
3742
3938
  generator;
@@ -3910,6 +4106,7 @@ var init_Pipeline = __esm({
3910
4106
  this._ephemeralHints = null;
3911
4107
  cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
3912
4108
  }
4109
+ cards = diversityRerank(cards);
3913
4110
  cards.sort((a, b) => b.score - a.score);
3914
4111
  const tFilter = performance.now();
3915
4112
  const result = cards.slice(0, limit);
@@ -4478,6 +4675,7 @@ var init_3 = __esm({
4478
4675
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
4479
4676
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
4480
4677
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
4678
+ "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
4481
4679
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
4482
4680
  "./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
4483
4681
  "./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
@@ -4503,9 +4701,12 @@ var init_3 = __esm({
4503
4701
  var navigators_exports = {};
4504
4702
  __export(navigators_exports, {
4505
4703
  ContentNavigator: () => ContentNavigator,
4704
+ DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
4705
+ DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
4506
4706
  NavigatorRole: () => NavigatorRole,
4507
4707
  NavigatorRoles: () => NavigatorRoles,
4508
4708
  Navigators: () => Navigators,
4709
+ diversityRerank: () => diversityRerank,
4509
4710
  getCardOrigin: () => getCardOrigin,
4510
4711
  getRegisteredNavigator: () => getRegisteredNavigator,
4511
4712
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
@@ -4589,6 +4790,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
4589
4790
  var init_navigators = __esm({
4590
4791
  "src/core/navigators/index.ts"() {
4591
4792
  "use strict";
4793
+ init_diversityRerank();
4592
4794
  init_PipelineDebugger();
4593
4795
  init_logger();
4594
4796
  init_();