@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
|
@@ -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
|
-
|
|
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_();
|