@vue-skuilder/db 0.2.4 → 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 +217 -8
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +214 -8
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +211 -8
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +211 -8
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +211 -8
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +211 -8
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +59 -1
- package/dist/index.d.ts +59 -1
- package/dist/index.js +444 -25
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +441 -25
- 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/elo.ts +32 -11
- 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 +123 -7
- package/src/study/SessionOverlay.ts +245 -21
package/dist/index.js
CHANGED
|
@@ -872,6 +872,95 @@ var init_courseLookupDB = __esm({
|
|
|
872
872
|
}
|
|
873
873
|
});
|
|
874
874
|
|
|
875
|
+
// src/core/navigators/diversityRerank.ts
|
|
876
|
+
var diversityRerank_exports = {};
|
|
877
|
+
__export(diversityRerank_exports, {
|
|
878
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
879
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
880
|
+
diversityRerank: () => diversityRerank
|
|
881
|
+
});
|
|
882
|
+
function diversityRerank(cards, opts = {}) {
|
|
883
|
+
const strength = opts.strength ?? DIVERSITY_STRENGTH;
|
|
884
|
+
const floor = opts.floor ?? DIVERSITY_FLOOR;
|
|
885
|
+
const n = cards.length;
|
|
886
|
+
if (n <= 1) return cards;
|
|
887
|
+
const df = /* @__PURE__ */ new Map();
|
|
888
|
+
for (const card of cards) {
|
|
889
|
+
for (const tag of card.tags ?? []) {
|
|
890
|
+
df.set(tag, (df.get(tag) ?? 0) + 1);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
const idf = /* @__PURE__ */ new Map();
|
|
894
|
+
for (const [tag, freq] of df) {
|
|
895
|
+
idf.set(tag, Math.log(n / freq));
|
|
896
|
+
}
|
|
897
|
+
const remaining = [...cards];
|
|
898
|
+
const emittedCount = /* @__PURE__ */ new Map();
|
|
899
|
+
const out = [];
|
|
900
|
+
const repetitionLoad = (card) => {
|
|
901
|
+
let load = 0;
|
|
902
|
+
for (const tag of card.tags ?? []) {
|
|
903
|
+
const seen = emittedCount.get(tag);
|
|
904
|
+
if (seen) load += (idf.get(tag) ?? 0) * seen;
|
|
905
|
+
}
|
|
906
|
+
return load;
|
|
907
|
+
};
|
|
908
|
+
while (remaining.length > 0) {
|
|
909
|
+
let bestIdx = 0;
|
|
910
|
+
let bestValue = -Infinity;
|
|
911
|
+
let bestPenalty = 1;
|
|
912
|
+
let bestLoad = 0;
|
|
913
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
914
|
+
const card = remaining[i];
|
|
915
|
+
const load = repetitionLoad(card);
|
|
916
|
+
const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
|
|
917
|
+
const value = card.score * penalty;
|
|
918
|
+
if (value > bestValue) {
|
|
919
|
+
bestValue = value;
|
|
920
|
+
bestIdx = i;
|
|
921
|
+
bestPenalty = penalty;
|
|
922
|
+
bestLoad = load;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
const [picked] = remaining.splice(bestIdx, 1);
|
|
926
|
+
if (Number.isFinite(picked.score) && bestPenalty < 1) {
|
|
927
|
+
const newScore = picked.score * bestPenalty;
|
|
928
|
+
out.push({
|
|
929
|
+
...picked,
|
|
930
|
+
score: newScore,
|
|
931
|
+
provenance: [
|
|
932
|
+
...picked.provenance,
|
|
933
|
+
{
|
|
934
|
+
strategy: STRATEGY,
|
|
935
|
+
strategyId: STRATEGY_ID,
|
|
936
|
+
strategyName: STRATEGY_NAME,
|
|
937
|
+
action: "penalized",
|
|
938
|
+
score: newScore,
|
|
939
|
+
reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
|
|
940
|
+
}
|
|
941
|
+
]
|
|
942
|
+
});
|
|
943
|
+
} else {
|
|
944
|
+
out.push(picked);
|
|
945
|
+
}
|
|
946
|
+
for (const tag of picked.tags ?? []) {
|
|
947
|
+
emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return out;
|
|
951
|
+
}
|
|
952
|
+
var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
|
|
953
|
+
var init_diversityRerank = __esm({
|
|
954
|
+
"src/core/navigators/diversityRerank.ts"() {
|
|
955
|
+
"use strict";
|
|
956
|
+
DIVERSITY_STRENGTH = 0.6;
|
|
957
|
+
DIVERSITY_FLOOR = 0.3;
|
|
958
|
+
STRATEGY = "diversityRerank";
|
|
959
|
+
STRATEGY_ID = "DIVERSITY_RERANK";
|
|
960
|
+
STRATEGY_NAME = "Diversity Re-rank";
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
|
|
875
964
|
// src/core/navigators/PipelineDebugger.ts
|
|
876
965
|
var PipelineDebugger_exports = {};
|
|
877
966
|
__export(PipelineDebugger_exports, {
|
|
@@ -1939,13 +2028,14 @@ var elo_exports = {};
|
|
|
1939
2028
|
__export(elo_exports, {
|
|
1940
2029
|
default: () => ELONavigator
|
|
1941
2030
|
});
|
|
1942
|
-
var import_common5, ELONavigator;
|
|
2031
|
+
var import_common5, ELO_RELEVANCE_SIGMA, ELONavigator;
|
|
1943
2032
|
var init_elo = __esm({
|
|
1944
2033
|
"src/core/navigators/generators/elo.ts"() {
|
|
1945
2034
|
"use strict";
|
|
1946
2035
|
init_navigators();
|
|
1947
2036
|
import_common5 = require("@vue-skuilder/common");
|
|
1948
2037
|
init_logger();
|
|
2038
|
+
ELO_RELEVANCE_SIGMA = 300;
|
|
1949
2039
|
ELONavigator = class extends ContentNavigator {
|
|
1950
2040
|
/** Human-readable name for CardGenerator interface */
|
|
1951
2041
|
name;
|
|
@@ -1985,8 +2075,8 @@ var init_elo = __esm({
|
|
|
1985
2075
|
const scored = newCards.map((c) => {
|
|
1986
2076
|
const cardElo = c.elo ?? 1e3;
|
|
1987
2077
|
const distance = Math.abs(cardElo - userGlobalElo);
|
|
1988
|
-
const
|
|
1989
|
-
const samplingKey =
|
|
2078
|
+
const relevance = Math.exp(-((distance / ELO_RELEVANCE_SIGMA) ** 2));
|
|
2079
|
+
const samplingKey = relevance * (0.5 + 0.5 * Math.random());
|
|
1990
2080
|
return {
|
|
1991
2081
|
cardId: c.cardID,
|
|
1992
2082
|
courseId: c.courseID,
|
|
@@ -1998,7 +2088,7 @@ var init_elo = __esm({
|
|
|
1998
2088
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-ELO-default",
|
|
1999
2089
|
action: "generated",
|
|
2000
2090
|
score: samplingKey,
|
|
2001
|
-
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}),
|
|
2091
|
+
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), relevance ${relevance.toFixed(3)}, key ${samplingKey.toFixed(3)}`
|
|
2002
2092
|
}
|
|
2003
2093
|
]
|
|
2004
2094
|
};
|
|
@@ -2066,7 +2156,7 @@ function shuffleInPlace(arr) {
|
|
|
2066
2156
|
function pickTopByScore(cards, limit) {
|
|
2067
2157
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
2068
2158
|
}
|
|
2069
|
-
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;
|
|
2159
|
+
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;
|
|
2070
2160
|
var init_prescribed = __esm({
|
|
2071
2161
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
2072
2162
|
"use strict";
|
|
@@ -2077,9 +2167,12 @@ var init_prescribed = __esm({
|
|
|
2077
2167
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
2078
2168
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
2079
2169
|
DEFAULT_MIN_COUNT = 3;
|
|
2170
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
2171
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
2080
2172
|
BASE_TARGET_SCORE = 1;
|
|
2081
2173
|
BASE_SUPPORT_SCORE = 0.8;
|
|
2082
2174
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
2175
|
+
BASE_PRACTICE_SCORE = 1;
|
|
2083
2176
|
MAX_TARGET_MULTIPLIER = 8;
|
|
2084
2177
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
2085
2178
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -2187,7 +2280,18 @@ var init_prescribed = __esm({
|
|
|
2187
2280
|
courseId,
|
|
2188
2281
|
emittedIds
|
|
2189
2282
|
);
|
|
2190
|
-
|
|
2283
|
+
const practiceCards = this.buildPracticeCards({
|
|
2284
|
+
group,
|
|
2285
|
+
courseId,
|
|
2286
|
+
emittedIds,
|
|
2287
|
+
cardsByTag,
|
|
2288
|
+
hierarchyConfigs,
|
|
2289
|
+
userTagElo,
|
|
2290
|
+
userGlobalElo,
|
|
2291
|
+
activeIds,
|
|
2292
|
+
seenIds
|
|
2293
|
+
});
|
|
2294
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
2191
2295
|
}
|
|
2192
2296
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2193
2297
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -2215,6 +2319,7 @@ var init_prescribed = __esm({
|
|
|
2215
2319
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
2216
2320
|
for (const card of finalCards) {
|
|
2217
2321
|
const prov = card.provenance[0];
|
|
2322
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
2218
2323
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
2219
2324
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
2220
2325
|
if (!groupId) continue;
|
|
@@ -2284,7 +2389,12 @@ var init_prescribed = __esm({
|
|
|
2284
2389
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
2285
2390
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
2286
2391
|
},
|
|
2287
|
-
retireOnEncounter: raw.retireOnEncounter !== false
|
|
2392
|
+
retireOnEncounter: raw.retireOnEncounter !== false,
|
|
2393
|
+
practiceTagPatterns: dedupe(
|
|
2394
|
+
Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
|
|
2395
|
+
),
|
|
2396
|
+
practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
|
|
2397
|
+
maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
|
|
2288
2398
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
2289
2399
|
return { groups };
|
|
2290
2400
|
} catch {
|
|
@@ -2507,6 +2617,92 @@ var init_prescribed = __esm({
|
|
|
2507
2617
|
}
|
|
2508
2618
|
return cards;
|
|
2509
2619
|
}
|
|
2620
|
+
/**
|
|
2621
|
+
* Emit drill cards for *unlocked-but-under-practiced* skills.
|
|
2622
|
+
*
|
|
2623
|
+
* For each course tag matching the group's `practiceTagPatterns` that is both
|
|
2624
|
+
* unlocked (all hierarchy prerequisites met — i.e. the learner has been
|
|
2625
|
+
* introduced to it) and under-practiced (per-tag attempt count below
|
|
2626
|
+
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2627
|
+
* into the candidate pool. It exists because global-ELO retrieval
|
|
2628
|
+
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2629
|
+
* freshly-introduced skill — putting them in the pool here lets the pipeline's
|
|
2630
|
+
* scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
|
|
2631
|
+
* this method's job; it only guarantees presence.
|
|
2632
|
+
*
|
|
2633
|
+
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2634
|
+
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
2635
|
+
*/
|
|
2636
|
+
buildPracticeCards(args) {
|
|
2637
|
+
const {
|
|
2638
|
+
group,
|
|
2639
|
+
courseId,
|
|
2640
|
+
emittedIds,
|
|
2641
|
+
cardsByTag,
|
|
2642
|
+
hierarchyConfigs,
|
|
2643
|
+
userTagElo,
|
|
2644
|
+
userGlobalElo,
|
|
2645
|
+
activeIds,
|
|
2646
|
+
seenIds
|
|
2647
|
+
} = args;
|
|
2648
|
+
const patterns = group.practiceTagPatterns ?? [];
|
|
2649
|
+
if (patterns.length === 0) return [];
|
|
2650
|
+
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2651
|
+
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2652
|
+
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2653
|
+
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2654
|
+
);
|
|
2655
|
+
if (practiceTags.length === 0) return [];
|
|
2656
|
+
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2657
|
+
supportTags: practiceTags,
|
|
2658
|
+
cardsByTag,
|
|
2659
|
+
activeIds,
|
|
2660
|
+
seenIds,
|
|
2661
|
+
excludedIds: emittedIds,
|
|
2662
|
+
limit: maxPractice
|
|
2663
|
+
});
|
|
2664
|
+
if (practiceCardIds.length === 0) return [];
|
|
2665
|
+
logger.info(
|
|
2666
|
+
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2667
|
+
);
|
|
2668
|
+
const cards = [];
|
|
2669
|
+
for (const cardId of practiceCardIds) {
|
|
2670
|
+
emittedIds.add(cardId);
|
|
2671
|
+
cards.push({
|
|
2672
|
+
cardId,
|
|
2673
|
+
courseId,
|
|
2674
|
+
score: BASE_PRACTICE_SCORE,
|
|
2675
|
+
provenance: [
|
|
2676
|
+
{
|
|
2677
|
+
strategy: "prescribed",
|
|
2678
|
+
strategyName: this.strategyName || this.name,
|
|
2679
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2680
|
+
action: "generated",
|
|
2681
|
+
score: BASE_PRACTICE_SCORE,
|
|
2682
|
+
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2683
|
+
}
|
|
2684
|
+
]
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
return cards;
|
|
2688
|
+
}
|
|
2689
|
+
/**
|
|
2690
|
+
* True for a skill that was *gated and is now reached*: it has at least one
|
|
2691
|
+
* declared hierarchy prerequisite set, and every set is fully satisfied by the
|
|
2692
|
+
* learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
|
|
2693
|
+
* — an ungated tag was never "introduced" in the curricular sense, so it isn't
|
|
2694
|
+
* a post-intro drill target (e.g. whole-word spelling tags that share the
|
|
2695
|
+
* `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
|
|
2696
|
+
* ELO retrieval. This is the precise population the retrieval gap strands:
|
|
2697
|
+
* just-unlocked, low-ELO skills.
|
|
2698
|
+
*/
|
|
2699
|
+
isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
|
|
2700
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
2701
|
+
if (prereqSets.length === 0) return false;
|
|
2702
|
+
return prereqSets.every(
|
|
2703
|
+
(prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
2704
|
+
);
|
|
2705
|
+
}
|
|
2510
2706
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2511
2707
|
if (supportTags.length === 0) {
|
|
2512
2708
|
return [];
|
|
@@ -4300,7 +4496,7 @@ function logResultCards(cards) {
|
|
|
4300
4496
|
for (let i = 0; i < cards.length; i++) {
|
|
4301
4497
|
const c = cards[i];
|
|
4302
4498
|
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
4303
|
-
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
4499
|
+
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) => {
|
|
4304
4500
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
4305
4501
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
4306
4502
|
}).join(" | ");
|
|
@@ -4332,6 +4528,7 @@ var init_Pipeline = __esm({
|
|
|
4332
4528
|
init_logger();
|
|
4333
4529
|
init_orchestration();
|
|
4334
4530
|
init_PipelineDebugger();
|
|
4531
|
+
init_diversityRerank();
|
|
4335
4532
|
VERBOSE_RESULTS = true;
|
|
4336
4533
|
Pipeline = class extends ContentNavigator {
|
|
4337
4534
|
generator;
|
|
@@ -4505,6 +4702,7 @@ var init_Pipeline = __esm({
|
|
|
4505
4702
|
this._ephemeralHints = null;
|
|
4506
4703
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
4507
4704
|
}
|
|
4705
|
+
cards = diversityRerank(cards);
|
|
4508
4706
|
cards.sort((a, b) => b.score - a.score);
|
|
4509
4707
|
const tFilter = performance.now();
|
|
4510
4708
|
const result = cards.slice(0, limit);
|
|
@@ -5073,6 +5271,7 @@ var init_3 = __esm({
|
|
|
5073
5271
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
5074
5272
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
5075
5273
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5274
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
5076
5275
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
5077
5276
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
5078
5277
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -5098,9 +5297,12 @@ var init_3 = __esm({
|
|
|
5098
5297
|
var navigators_exports = {};
|
|
5099
5298
|
__export(navigators_exports, {
|
|
5100
5299
|
ContentNavigator: () => ContentNavigator,
|
|
5300
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
5301
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
5101
5302
|
NavigatorRole: () => NavigatorRole,
|
|
5102
5303
|
NavigatorRoles: () => NavigatorRoles,
|
|
5103
5304
|
Navigators: () => Navigators,
|
|
5305
|
+
diversityRerank: () => diversityRerank,
|
|
5104
5306
|
getCardOrigin: () => getCardOrigin,
|
|
5105
5307
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
5106
5308
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -5184,6 +5386,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
5184
5386
|
var init_navigators = __esm({
|
|
5185
5387
|
"src/core/navigators/index.ts"() {
|
|
5186
5388
|
"use strict";
|
|
5389
|
+
init_diversityRerank();
|
|
5187
5390
|
init_PipelineDebugger();
|
|
5188
5391
|
init_logger();
|
|
5189
5392
|
init_();
|
|
@@ -10186,6 +10389,8 @@ __export(index_exports, {
|
|
|
10186
10389
|
ContentNavigator: () => ContentNavigator,
|
|
10187
10390
|
CouchDBToStaticPacker: () => CouchDBToStaticPacker,
|
|
10188
10391
|
CourseLookup: () => CourseLookup,
|
|
10392
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
10393
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
10189
10394
|
DocType: () => DocType,
|
|
10190
10395
|
DocTypePrefixes: () => DocTypePrefixes,
|
|
10191
10396
|
ENV: () => ENV,
|
|
@@ -10211,6 +10416,7 @@ __export(index_exports, {
|
|
|
10211
10416
|
computeSpread: () => computeSpread,
|
|
10212
10417
|
computeStrategyGradient: () => computeStrategyGradient,
|
|
10213
10418
|
createOrchestrationContext: () => createOrchestrationContext,
|
|
10419
|
+
diversityRerank: () => diversityRerank,
|
|
10214
10420
|
docIsDeleted: () => docIsDeleted,
|
|
10215
10421
|
endSessionTracking: () => endSessionTracking,
|
|
10216
10422
|
ensureAppDataDirectory: () => ensureAppDataDirectory,
|
|
@@ -11322,8 +11528,17 @@ var ItemQueue = class {
|
|
|
11322
11528
|
* Merge new items into the front of the queue, skipping duplicates.
|
|
11323
11529
|
* Used by additive replans to inject high-quality candidates without
|
|
11324
11530
|
* discarding the existing queue contents.
|
|
11531
|
+
*
|
|
11532
|
+
* `forceFrontIds` carries the mandatory (`+INF`) cards in this batch — a
|
|
11533
|
+
* durable `requireCard`/`requireTag` re-asserted by every replan. An ordinary
|
|
11534
|
+
* duplicate is left in place (skip), but a mandatory one that's *already*
|
|
11535
|
+
* queued is pulled out of its current slot so it rejoins at the front in batch
|
|
11536
|
+
* order. Without this, an additive merge unshifts fresh non-required cards
|
|
11537
|
+
* ahead of an already-present required card, steadily burying it until it never
|
|
11538
|
+
* gets drawn — defeating the "must appear" guarantee. Returns the count of
|
|
11539
|
+
* genuinely new cards added (re-fronted duplicates are not counted).
|
|
11325
11540
|
*/
|
|
11326
|
-
mergeToFront(items, cardIdExtractor) {
|
|
11541
|
+
mergeToFront(items, cardIdExtractor, forceFrontIds) {
|
|
11327
11542
|
let added = 0;
|
|
11328
11543
|
const toInsert = [];
|
|
11329
11544
|
for (const item of items) {
|
|
@@ -11332,6 +11547,11 @@ var ItemQueue = class {
|
|
|
11332
11547
|
this.seenCardIds.push(cardId);
|
|
11333
11548
|
toInsert.push(item);
|
|
11334
11549
|
added++;
|
|
11550
|
+
} else if (forceFrontIds?.has(cardId)) {
|
|
11551
|
+
const idx = this.q.findIndex((qi) => cardIdExtractor(qi) === cardId);
|
|
11552
|
+
if (idx >= 0) {
|
|
11553
|
+
toInsert.push(...this.q.splice(idx, 1));
|
|
11554
|
+
}
|
|
11335
11555
|
}
|
|
11336
11556
|
}
|
|
11337
11557
|
this.q.unshift(...toInsert);
|
|
@@ -13173,7 +13393,15 @@ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834"
|
|
|
13173
13393
|
var spinnerFrame = 0;
|
|
13174
13394
|
var overlayEl = null;
|
|
13175
13395
|
var pollHandle = null;
|
|
13176
|
-
var
|
|
13396
|
+
var lastSnapshot = null;
|
|
13397
|
+
var copyFlashUntil = 0;
|
|
13398
|
+
var minified = false;
|
|
13399
|
+
var expanded = {
|
|
13400
|
+
reviewQ: false,
|
|
13401
|
+
newQ: false,
|
|
13402
|
+
failedQ: false,
|
|
13403
|
+
drawn: false
|
|
13404
|
+
};
|
|
13177
13405
|
function toggleSessionOverlay() {
|
|
13178
13406
|
if (typeof document === "undefined") {
|
|
13179
13407
|
logger.info("[Session Overlay] No DOM available (non-browser host); overlay unavailable.");
|
|
@@ -13188,6 +13416,7 @@ function toggleSessionOverlay() {
|
|
|
13188
13416
|
}
|
|
13189
13417
|
}
|
|
13190
13418
|
function mount() {
|
|
13419
|
+
minified = false;
|
|
13191
13420
|
overlayEl = document.createElement("div");
|
|
13192
13421
|
overlayEl.id = OVERLAY_ID;
|
|
13193
13422
|
Object.assign(overlayEl.style, {
|
|
@@ -13226,11 +13455,23 @@ function render() {
|
|
|
13226
13455
|
spinnerFrame++;
|
|
13227
13456
|
const ctrl = getActiveController();
|
|
13228
13457
|
if (!ctrl) {
|
|
13229
|
-
|
|
13458
|
+
lastSnapshot = null;
|
|
13459
|
+
overlayEl.innerHTML = headerHtml() + (minified ? "" : `<div style="opacity:.65">No active session.</div>`);
|
|
13460
|
+
attachHandlers();
|
|
13230
13461
|
return;
|
|
13231
13462
|
}
|
|
13232
13463
|
const s = ctrl.getDebugSnapshot();
|
|
13233
|
-
|
|
13464
|
+
lastSnapshot = s;
|
|
13465
|
+
if (minified) {
|
|
13466
|
+
overlayEl.innerHTML = headerHtml();
|
|
13467
|
+
attachHandlers();
|
|
13468
|
+
return;
|
|
13469
|
+
}
|
|
13470
|
+
overlayEl.innerHTML = headerHtml() + replanHtml(s) + metaHtml(s) + hintsHtml(s.sessionHints) + queueHtml("reviewQ", "reviewQ", s.reviewQ) + queueHtml("newQ", "newQ", s.newQ) + queueHtml("failedQ", "failedQ", s.failedQ) + drawnHtml("drawn", s.drawnCards);
|
|
13471
|
+
attachHandlers();
|
|
13472
|
+
}
|
|
13473
|
+
function attachHandlers() {
|
|
13474
|
+
if (!overlayEl) return;
|
|
13234
13475
|
overlayEl.querySelectorAll("[data-q]").forEach((el) => {
|
|
13235
13476
|
el.onclick = () => {
|
|
13236
13477
|
const key = el.dataset.q;
|
|
@@ -13239,9 +13480,45 @@ function render() {
|
|
|
13239
13480
|
render();
|
|
13240
13481
|
};
|
|
13241
13482
|
});
|
|
13483
|
+
const copyBtn = overlayEl.querySelector("[data-copy]");
|
|
13484
|
+
if (copyBtn) {
|
|
13485
|
+
copyBtn.onclick = (ev) => {
|
|
13486
|
+
ev.stopPropagation();
|
|
13487
|
+
copySnapshot();
|
|
13488
|
+
};
|
|
13489
|
+
}
|
|
13490
|
+
const minBtn = overlayEl.querySelector("[data-min]");
|
|
13491
|
+
if (minBtn) {
|
|
13492
|
+
minBtn.onclick = (ev) => {
|
|
13493
|
+
ev.stopPropagation();
|
|
13494
|
+
minified = !minified;
|
|
13495
|
+
render();
|
|
13496
|
+
};
|
|
13497
|
+
}
|
|
13498
|
+
}
|
|
13499
|
+
function copySnapshot() {
|
|
13500
|
+
const text = snapshotToText(lastSnapshot);
|
|
13501
|
+
const flash = () => {
|
|
13502
|
+
copyFlashUntil = Date.now() + 1200;
|
|
13503
|
+
render();
|
|
13504
|
+
};
|
|
13505
|
+
const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
13506
|
+
if (clip?.writeText) {
|
|
13507
|
+
clip.writeText(text).then(flash, (err) => {
|
|
13508
|
+
logger.warn(`[Session Overlay] Clipboard write failed: ${String(err)}`);
|
|
13509
|
+
});
|
|
13510
|
+
} else {
|
|
13511
|
+
logger.info(`[Session Overlay] Clipboard unavailable; snapshot follows:
|
|
13512
|
+
${text}`);
|
|
13513
|
+
}
|
|
13242
13514
|
}
|
|
13243
13515
|
function headerHtml() {
|
|
13244
|
-
|
|
13516
|
+
const flashing = Date.now() < copyFlashUntil;
|
|
13517
|
+
const btnLabel = flashing ? "\u2713 copied" : "\u2398 copy";
|
|
13518
|
+
const btnColor = flashing ? "#86efac" : "#93c5fd";
|
|
13519
|
+
const copyBtn = `<span data-copy style="cursor:pointer;float:right;font-weight:400;color:${btnColor};border:1px solid currentColor;border-radius:4px;padding:0 4px;line-height:1.3">${btnLabel}</span>`;
|
|
13520
|
+
const minBtn = `<span data-min title="${minified ? "Restore" : "Minify"}" style="cursor:pointer;float:right;font-weight:400;color:#93c5fd;border:1px solid currentColor;border-radius:4px;padding:0 5px;margin-right:4px;line-height:1.3">${minified ? "\u25A2" : "\u2014"}</span>`;
|
|
13521
|
+
return `<div style="font-weight:600;color:#93c5fd;margin-bottom:${minified ? 0 : 4}px">${copyBtn}${minBtn}\u2699 SessionController</div>`;
|
|
13245
13522
|
}
|
|
13246
13523
|
function replanHtml(s) {
|
|
13247
13524
|
if (!s.replanActive) {
|
|
@@ -13284,22 +13561,98 @@ function hintsHtml(h) {
|
|
|
13284
13561
|
}
|
|
13285
13562
|
function queueHtml(key, label, q) {
|
|
13286
13563
|
const collapsible = q.length > INLINE_THRESHOLD;
|
|
13287
|
-
const isOpen =
|
|
13564
|
+
const isOpen = collapsible && expanded[key];
|
|
13288
13565
|
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13289
13566
|
const drawn = q.dequeueCount ? ` <span style="opacity:.5">drawn ${q.dequeueCount}</span>` : "";
|
|
13290
13567
|
const titleStyle = collapsible ? "cursor:pointer;color:#f9a8d4" : "color:#f9a8d4";
|
|
13291
13568
|
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13292
13569
|
const title = `<div${titleAttr} style="${titleStyle}">${caret}${label}: ${q.length}${drawn}</div>`;
|
|
13293
|
-
|
|
13294
|
-
|
|
13295
|
-
|
|
13296
|
-
|
|
13297
|
-
|
|
13298
|
-
|
|
13299
|
-
|
|
13570
|
+
if (!q.cards.length) {
|
|
13571
|
+
return title + `<div style="margin:1px 0 6px 6px;opacity:.5">empty</div>`;
|
|
13572
|
+
}
|
|
13573
|
+
const shown = isOpen ? q.cards : q.cards.slice(0, INLINE_THRESHOLD);
|
|
13574
|
+
const hiddenCount = q.length - shown.length;
|
|
13575
|
+
const listMarginBottom = collapsible ? 2 : 6;
|
|
13576
|
+
let body = `<ol style="margin:2px 0 ${listMarginBottom}px 0;padding-left:20px">` + shown.map((c) => `<li style="white-space:nowrap">${esc(c)}</li>`).join("") + `</ol>`;
|
|
13577
|
+
if (collapsible) {
|
|
13578
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13579
|
+
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
13300
13580
|
}
|
|
13301
13581
|
return title + body;
|
|
13302
13582
|
}
|
|
13583
|
+
function outcomeGlyph(correct) {
|
|
13584
|
+
if (correct === true) return `<span style="color:#86efac">\u2713</span>`;
|
|
13585
|
+
if (correct === false) return `<span style="color:#fca5a5">\u2717</span>`;
|
|
13586
|
+
return `<span style="opacity:.5">\xB7</span>`;
|
|
13587
|
+
}
|
|
13588
|
+
function drawnHtml(key, drawn) {
|
|
13589
|
+
const collapsible = drawn.length > INLINE_THRESHOLD;
|
|
13590
|
+
const isOpen = collapsible && expanded[key];
|
|
13591
|
+
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13592
|
+
const titleStyle = collapsible ? "cursor:pointer;color:#c4b5fd" : "color:#c4b5fd";
|
|
13593
|
+
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13594
|
+
const title = `<div${titleAttr} style="${titleStyle}">${caret}drawn: ${drawn.length}</div>`;
|
|
13595
|
+
if (!drawn.length) {
|
|
13596
|
+
return title + `<div style="margin:1px 0 6px 6px;opacity:.5">none yet</div>`;
|
|
13597
|
+
}
|
|
13598
|
+
const shown = isOpen ? drawn : drawn.slice(0, INLINE_THRESHOLD);
|
|
13599
|
+
const hiddenCount = drawn.length - shown.length;
|
|
13600
|
+
const listMarginBottom = collapsible ? 2 : 6;
|
|
13601
|
+
const rows = shown.map((d) => {
|
|
13602
|
+
const retries = d.attempts > 1 ? `<span style="opacity:.5"> \xD7${d.attempts}</span>` : "";
|
|
13603
|
+
const time = `<span style="opacity:.45"> ${Math.round(d.timeSpentMs / 100) / 10}s</span>`;
|
|
13604
|
+
return `<li style="white-space:nowrap">${outcomeGlyph(d.correct)} ${esc(d.cardID)}<span style="opacity:.5"> [${esc(d.status)}]</span>${retries}${time}</li>`;
|
|
13605
|
+
}).join("");
|
|
13606
|
+
let body = `<ol style="margin:2px 0 ${listMarginBottom}px 0;padding-left:20px">${rows}</ol>`;
|
|
13607
|
+
if (collapsible) {
|
|
13608
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13609
|
+
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
13610
|
+
}
|
|
13611
|
+
return title + body;
|
|
13612
|
+
}
|
|
13613
|
+
function snapshotToText(s) {
|
|
13614
|
+
if (!s) return "SessionController \u2014 no active session.";
|
|
13615
|
+
const lines = [];
|
|
13616
|
+
lines.push("=== SessionController ===");
|
|
13617
|
+
lines.push(`time ${formatTime(s.secondsRemaining)}`);
|
|
13618
|
+
if (s.hasCardGuarantee) lines.push(`guarantee: ${s.minCardsGuarantee}`);
|
|
13619
|
+
lines.push(`well-indicated left: ${s.wellIndicatedRemaining}`);
|
|
13620
|
+
lines.push(`current: ${s.currentCard ?? "\u2014"}`);
|
|
13621
|
+
lines.push(
|
|
13622
|
+
s.replanActive ? `replan: ACTIVE [${s.replanLabel ?? "(auto)"}]` : "replan: idle"
|
|
13623
|
+
);
|
|
13624
|
+
lines.push("");
|
|
13625
|
+
lines.push("sessionHints:");
|
|
13626
|
+
const h = s.sessionHints;
|
|
13627
|
+
const hintParts = [];
|
|
13628
|
+
if (h) {
|
|
13629
|
+
if (h.boostTags && Object.keys(h.boostTags).length)
|
|
13630
|
+
hintParts.push(` boost: ${Object.entries(h.boostTags).map(([k, v]) => `${k}\xD7${v}`).join(", ")}`);
|
|
13631
|
+
if (h.boostCards && Object.keys(h.boostCards).length)
|
|
13632
|
+
hintParts.push(` boostCards: ${Object.entries(h.boostCards).map(([k, v]) => `${k}\xD7${v}`).join(", ")}`);
|
|
13633
|
+
if (h.requireCards?.length) hintParts.push(` require: ${h.requireCards.join(", ")}`);
|
|
13634
|
+
if (h.requireTags?.length) hintParts.push(` requireTags: ${h.requireTags.join(", ")}`);
|
|
13635
|
+
if (h.excludeTags?.length) hintParts.push(` exclude: ${h.excludeTags.join(", ")}`);
|
|
13636
|
+
if (h.excludeCards?.length) hintParts.push(` excludeCards: ${h.excludeCards.join(", ")}`);
|
|
13637
|
+
}
|
|
13638
|
+
lines.push(hintParts.length ? hintParts.join("\n") : " none");
|
|
13639
|
+
const queueText = (label, q) => {
|
|
13640
|
+
lines.push("");
|
|
13641
|
+
lines.push(`${label}: ${q.length} (drawn ${q.dequeueCount})`);
|
|
13642
|
+
q.cards.forEach((c, i) => lines.push(` ${i + 1}. ${c}`));
|
|
13643
|
+
};
|
|
13644
|
+
queueText("reviewQ", s.reviewQ);
|
|
13645
|
+
queueText("newQ", s.newQ);
|
|
13646
|
+
queueText("failedQ", s.failedQ);
|
|
13647
|
+
lines.push("");
|
|
13648
|
+
lines.push(`drawn: ${s.drawnCards.length}`);
|
|
13649
|
+
s.drawnCards.forEach((d, i) => {
|
|
13650
|
+
const mark = d.correct === true ? "\u2713" : d.correct === false ? "\u2717" : "\xB7";
|
|
13651
|
+
const time = `${Math.round(d.timeSpentMs / 100) / 10}s`;
|
|
13652
|
+
lines.push(` ${i + 1}. ${mark} ${d.cardID} [${d.status}] \xD7${d.attempts} ${time}`);
|
|
13653
|
+
});
|
|
13654
|
+
return lines.join("\n");
|
|
13655
|
+
}
|
|
13303
13656
|
function formatTime(totalSeconds) {
|
|
13304
13657
|
const s = Math.max(0, Math.round(totalSeconds));
|
|
13305
13658
|
const m = Math.floor(s / 60);
|
|
@@ -13669,6 +14022,21 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13669
14022
|
* recomputed per-run in `_runReplan` and would otherwise go stale.
|
|
13670
14023
|
*/
|
|
13671
14024
|
_sessionHints = null;
|
|
14025
|
+
/**
|
|
14026
|
+
* Card IDs that have been *served* (drawn/consumed) this session. Populated
|
|
14027
|
+
* at the single consumption choke-point (removeItemFromQueue), so it reflects
|
|
14028
|
+
* a draw the instant it happens — earlier than `_sessionRecord`, which only
|
|
14029
|
+
* lands once the card is *responded to*.
|
|
14030
|
+
*
|
|
14031
|
+
* Used to keep already-served cards out of newQ on every (re)plan: a `new`
|
|
14032
|
+
* card shown once must never re-enter newQ this session. This is the general
|
|
14033
|
+
* guard against re-presentation — including the case where a replan in flight
|
|
14034
|
+
* captured a now-drawn card (e.g. a +INF require-injected follow-up the
|
|
14035
|
+
* depletion prefetch grabbed just before it was drawn). Reviews/failed cards
|
|
14036
|
+
* legitimately recur and are tracked by their own queues, so this only gates
|
|
14037
|
+
* `new`-origin candidates.
|
|
14038
|
+
*/
|
|
14039
|
+
_servedCardIds = /* @__PURE__ */ new Set();
|
|
13672
14040
|
/**
|
|
13673
14041
|
* Consumer-supplied hooks invoked after each question response is processed.
|
|
13674
14042
|
* Seeded from constructor options (threaded from
|
|
@@ -13878,6 +14246,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13878
14246
|
if (opts.mode && opts.mode !== "replace") return true;
|
|
13879
14247
|
if (opts.hints && Object.keys(opts.hints).length > 0) return true;
|
|
13880
14248
|
if (opts.sessionHints !== void 0) return true;
|
|
14249
|
+
if (opts.mergeSessionHints !== void 0) return true;
|
|
13881
14250
|
return false;
|
|
13882
14251
|
}
|
|
13883
14252
|
/**
|
|
@@ -13913,6 +14282,10 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13913
14282
|
`[Replan] Session hints ${opts.sessionHints ? "set" : "cleared"}: ${JSON.stringify(opts.sessionHints)}`
|
|
13914
14283
|
);
|
|
13915
14284
|
}
|
|
14285
|
+
if (opts.mergeSessionHints !== void 0) {
|
|
14286
|
+
this._sessionHints = mergeHints2([this._sessionHints, opts.mergeSessionHints]) ?? null;
|
|
14287
|
+
this.log(`[Replan] Session hints merged: ${JSON.stringify(this._sessionHints)}`);
|
|
14288
|
+
}
|
|
13916
14289
|
this._applyHintsToSources(opts.hints, opts.label);
|
|
13917
14290
|
const labelTag = opts.label ? ` [${opts.label}]` : "";
|
|
13918
14291
|
this.log(
|
|
@@ -13965,6 +14338,16 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13965
14338
|
}
|
|
13966
14339
|
return { length: q.length, dequeueCount: q.dequeueCount, cards };
|
|
13967
14340
|
};
|
|
14341
|
+
const drawnCards = this._sessionRecord.map((r) => {
|
|
14342
|
+
const last = r.records[r.records.length - 1];
|
|
14343
|
+
return {
|
|
14344
|
+
cardID: r.item.cardID,
|
|
14345
|
+
status: r.item.status,
|
|
14346
|
+
attempts: r.records.length,
|
|
14347
|
+
correct: last && isQuestionRecord(last) ? last.isCorrect : null,
|
|
14348
|
+
timeSpentMs: r.records.reduce((sum, rec) => sum + rec.timeSpent, 0)
|
|
14349
|
+
};
|
|
14350
|
+
});
|
|
13968
14351
|
return {
|
|
13969
14352
|
secondsRemaining: this.secondsRemaining,
|
|
13970
14353
|
hasCardGuarantee: this.hasCardGuarantee,
|
|
@@ -13976,7 +14359,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13976
14359
|
replanLabel: this._activeReplanLabel,
|
|
13977
14360
|
reviewQ: describe(this.reviewQ),
|
|
13978
14361
|
newQ: describe(this.newQ),
|
|
13979
|
-
failedQ: describe(this.failedQ)
|
|
14362
|
+
failedQ: describe(this.failedQ),
|
|
14363
|
+
drawnCards
|
|
13980
14364
|
};
|
|
13981
14365
|
}
|
|
13982
14366
|
/**
|
|
@@ -14285,7 +14669,16 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14285
14669
|
mixedWeighted
|
|
14286
14670
|
);
|
|
14287
14671
|
const reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review").slice(0, this._initialReviewCap);
|
|
14288
|
-
const
|
|
14672
|
+
const newCandidates = mixedWeighted.filter(
|
|
14673
|
+
(w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)
|
|
14674
|
+
);
|
|
14675
|
+
const mandatoryWeighted = newCandidates.filter((w) => w.score === Number.POSITIVE_INFINITY);
|
|
14676
|
+
const optionalWeighted = newCandidates.filter((w) => w.score !== Number.POSITIVE_INFINITY);
|
|
14677
|
+
const newWeighted = [
|
|
14678
|
+
...mandatoryWeighted,
|
|
14679
|
+
...optionalWeighted.slice(0, Math.max(0, newLimit - mandatoryWeighted.length))
|
|
14680
|
+
];
|
|
14681
|
+
const mandatoryIds = new Set(mandatoryWeighted.map((w) => w.cardId));
|
|
14289
14682
|
logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
|
|
14290
14683
|
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
14291
14684
|
if (!replan) {
|
|
@@ -14320,7 +14713,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14320
14713
|
`;
|
|
14321
14714
|
}
|
|
14322
14715
|
if (additive) {
|
|
14323
|
-
const added = this.newQ.mergeToFront(newItems, (item) => item.cardID);
|
|
14716
|
+
const added = this.newQ.mergeToFront(newItems, (item) => item.cardID, mandatoryIds);
|
|
14324
14717
|
report += `Additive merge: ${added} new cards added to front of newQ
|
|
14325
14718
|
`;
|
|
14326
14719
|
} else if (replan) {
|
|
@@ -14422,7 +14815,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14422
14815
|
this.log(
|
|
14423
14816
|
`[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
|
|
14424
14817
|
);
|
|
14425
|
-
void this.requestReplan({ label: "auto:depletion" });
|
|
14818
|
+
void this.requestReplan({ label: "auto:depletion", mode: "merge" });
|
|
14426
14819
|
}
|
|
14427
14820
|
const REPLAN_BUFFER = 3;
|
|
14428
14821
|
if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
|
|
@@ -14563,6 +14956,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14563
14956
|
* Remove an item from its source queue after consumption by nextCard().
|
|
14564
14957
|
*/
|
|
14565
14958
|
removeItemFromQueue(item) {
|
|
14959
|
+
this._clearDurableRequirement(item.cardID);
|
|
14960
|
+
this._servedCardIds.add(item.cardID);
|
|
14566
14961
|
if (this.reviewQ.peek(0)?.cardID === item.cardID) {
|
|
14567
14962
|
this.reviewQ.dequeue((queueItem) => queueItem.cardID);
|
|
14568
14963
|
} else if (this.newQ.peek(0)?.cardID === item.cardID) {
|
|
@@ -14574,6 +14969,27 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14574
14969
|
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
14575
14970
|
}
|
|
14576
14971
|
}
|
|
14972
|
+
/**
|
|
14973
|
+
* Remove a satisfied card ID from the durable session-hint `requireCards`
|
|
14974
|
+
* list. Called when a card is consumed (see removeItemFromQueue). No-op if
|
|
14975
|
+
* the card was not a durable requirement.
|
|
14976
|
+
*
|
|
14977
|
+
* Matches literal IDs only: a glob/pattern requirement (which may stand for
|
|
14978
|
+
* several cards) is NOT considered satisfied by a single draw and is left in
|
|
14979
|
+
* place — durable patterns are the caller's responsibility, one-shot `hints`
|
|
14980
|
+
* remain the right tool for them.
|
|
14981
|
+
*/
|
|
14982
|
+
_clearDurableRequirement(cardID) {
|
|
14983
|
+
const req = this._sessionHints?.requireCards;
|
|
14984
|
+
if (!req || req.length === 0) return;
|
|
14985
|
+
const next = req.filter((id) => id !== cardID);
|
|
14986
|
+
if (next.length === req.length) return;
|
|
14987
|
+
this._sessionHints = {
|
|
14988
|
+
...this._sessionHints,
|
|
14989
|
+
requireCards: next.length > 0 ? next : void 0
|
|
14990
|
+
};
|
|
14991
|
+
this.log(`[Replan] Durable requirement satisfied & cleared on draw: ${cardID}`);
|
|
14992
|
+
}
|
|
14577
14993
|
/**
|
|
14578
14994
|
* End the session and record learning outcomes.
|
|
14579
14995
|
*
|
|
@@ -14629,6 +15045,8 @@ init_factory();
|
|
|
14629
15045
|
ContentNavigator,
|
|
14630
15046
|
CouchDBToStaticPacker,
|
|
14631
15047
|
CourseLookup,
|
|
15048
|
+
DIVERSITY_FLOOR,
|
|
15049
|
+
DIVERSITY_STRENGTH,
|
|
14632
15050
|
DocType,
|
|
14633
15051
|
DocTypePrefixes,
|
|
14634
15052
|
ENV,
|
|
@@ -14654,6 +15072,7 @@ init_factory();
|
|
|
14654
15072
|
computeSpread,
|
|
14655
15073
|
computeStrategyGradient,
|
|
14656
15074
|
createOrchestrationContext,
|
|
15075
|
+
diversityRerank,
|
|
14657
15076
|
docIsDeleted,
|
|
14658
15077
|
endSessionTracking,
|
|
14659
15078
|
ensureAppDataDirectory,
|