@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.
- package/dist/core/index.d.cts +37 -1
- package/dist/core/index.d.ts +37 -1
- package/dist/core/index.js +212 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +209 -4
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +206 -4
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +206 -4
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +206 -4
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +206 -4
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +238 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +235 -7
- package/dist/index.mjs.map +1 -1
- package/docs/navigators-architecture.md +42 -2
- package/package.json +3 -3
- package/src/core/navigators/Pipeline.ts +10 -1
- package/src/core/navigators/diversityRerank.ts +185 -0
- package/src/core/navigators/generators/prescribed.ts +173 -1
- package/src/core/navigators/index.ts +8 -0
- package/src/study/ItemQueue.test.ts +71 -0
- package/src/study/ItemQueue.ts +19 -1
- package/src/study/SessionController.ts +20 -5
|
@@ -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
|
-
|
|
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_();
|