@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.mjs
CHANGED
|
@@ -849,6 +849,95 @@ var init_courseLookupDB = __esm({
|
|
|
849
849
|
}
|
|
850
850
|
});
|
|
851
851
|
|
|
852
|
+
// src/core/navigators/diversityRerank.ts
|
|
853
|
+
var diversityRerank_exports = {};
|
|
854
|
+
__export(diversityRerank_exports, {
|
|
855
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
856
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
857
|
+
diversityRerank: () => diversityRerank
|
|
858
|
+
});
|
|
859
|
+
function diversityRerank(cards, opts = {}) {
|
|
860
|
+
const strength = opts.strength ?? DIVERSITY_STRENGTH;
|
|
861
|
+
const floor = opts.floor ?? DIVERSITY_FLOOR;
|
|
862
|
+
const n = cards.length;
|
|
863
|
+
if (n <= 1) return cards;
|
|
864
|
+
const df = /* @__PURE__ */ new Map();
|
|
865
|
+
for (const card of cards) {
|
|
866
|
+
for (const tag of card.tags ?? []) {
|
|
867
|
+
df.set(tag, (df.get(tag) ?? 0) + 1);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
const idf = /* @__PURE__ */ new Map();
|
|
871
|
+
for (const [tag, freq] of df) {
|
|
872
|
+
idf.set(tag, Math.log(n / freq));
|
|
873
|
+
}
|
|
874
|
+
const remaining = [...cards];
|
|
875
|
+
const emittedCount = /* @__PURE__ */ new Map();
|
|
876
|
+
const out = [];
|
|
877
|
+
const repetitionLoad = (card) => {
|
|
878
|
+
let load = 0;
|
|
879
|
+
for (const tag of card.tags ?? []) {
|
|
880
|
+
const seen = emittedCount.get(tag);
|
|
881
|
+
if (seen) load += (idf.get(tag) ?? 0) * seen;
|
|
882
|
+
}
|
|
883
|
+
return load;
|
|
884
|
+
};
|
|
885
|
+
while (remaining.length > 0) {
|
|
886
|
+
let bestIdx = 0;
|
|
887
|
+
let bestValue = -Infinity;
|
|
888
|
+
let bestPenalty = 1;
|
|
889
|
+
let bestLoad = 0;
|
|
890
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
891
|
+
const card = remaining[i];
|
|
892
|
+
const load = repetitionLoad(card);
|
|
893
|
+
const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
|
|
894
|
+
const value = card.score * penalty;
|
|
895
|
+
if (value > bestValue) {
|
|
896
|
+
bestValue = value;
|
|
897
|
+
bestIdx = i;
|
|
898
|
+
bestPenalty = penalty;
|
|
899
|
+
bestLoad = load;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
const [picked] = remaining.splice(bestIdx, 1);
|
|
903
|
+
if (Number.isFinite(picked.score) && bestPenalty < 1) {
|
|
904
|
+
const newScore = picked.score * bestPenalty;
|
|
905
|
+
out.push({
|
|
906
|
+
...picked,
|
|
907
|
+
score: newScore,
|
|
908
|
+
provenance: [
|
|
909
|
+
...picked.provenance,
|
|
910
|
+
{
|
|
911
|
+
strategy: STRATEGY,
|
|
912
|
+
strategyId: STRATEGY_ID,
|
|
913
|
+
strategyName: STRATEGY_NAME,
|
|
914
|
+
action: "penalized",
|
|
915
|
+
score: newScore,
|
|
916
|
+
reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
|
|
917
|
+
}
|
|
918
|
+
]
|
|
919
|
+
});
|
|
920
|
+
} else {
|
|
921
|
+
out.push(picked);
|
|
922
|
+
}
|
|
923
|
+
for (const tag of picked.tags ?? []) {
|
|
924
|
+
emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
return out;
|
|
928
|
+
}
|
|
929
|
+
var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
|
|
930
|
+
var init_diversityRerank = __esm({
|
|
931
|
+
"src/core/navigators/diversityRerank.ts"() {
|
|
932
|
+
"use strict";
|
|
933
|
+
DIVERSITY_STRENGTH = 0.6;
|
|
934
|
+
DIVERSITY_FLOOR = 0.3;
|
|
935
|
+
STRATEGY = "diversityRerank";
|
|
936
|
+
STRATEGY_ID = "DIVERSITY_RERANK";
|
|
937
|
+
STRATEGY_NAME = "Diversity Re-rank";
|
|
938
|
+
}
|
|
939
|
+
});
|
|
940
|
+
|
|
852
941
|
// src/core/navigators/PipelineDebugger.ts
|
|
853
942
|
var PipelineDebugger_exports = {};
|
|
854
943
|
__export(PipelineDebugger_exports, {
|
|
@@ -1917,12 +2006,13 @@ __export(elo_exports, {
|
|
|
1917
2006
|
default: () => ELONavigator
|
|
1918
2007
|
});
|
|
1919
2008
|
import { toCourseElo as toCourseElo2 } from "@vue-skuilder/common";
|
|
1920
|
-
var ELONavigator;
|
|
2009
|
+
var ELO_RELEVANCE_SIGMA, ELONavigator;
|
|
1921
2010
|
var init_elo = __esm({
|
|
1922
2011
|
"src/core/navigators/generators/elo.ts"() {
|
|
1923
2012
|
"use strict";
|
|
1924
2013
|
init_navigators();
|
|
1925
2014
|
init_logger();
|
|
2015
|
+
ELO_RELEVANCE_SIGMA = 300;
|
|
1926
2016
|
ELONavigator = class extends ContentNavigator {
|
|
1927
2017
|
/** Human-readable name for CardGenerator interface */
|
|
1928
2018
|
name;
|
|
@@ -1962,8 +2052,8 @@ var init_elo = __esm({
|
|
|
1962
2052
|
const scored = newCards.map((c) => {
|
|
1963
2053
|
const cardElo = c.elo ?? 1e3;
|
|
1964
2054
|
const distance = Math.abs(cardElo - userGlobalElo);
|
|
1965
|
-
const
|
|
1966
|
-
const samplingKey =
|
|
2055
|
+
const relevance = Math.exp(-((distance / ELO_RELEVANCE_SIGMA) ** 2));
|
|
2056
|
+
const samplingKey = relevance * (0.5 + 0.5 * Math.random());
|
|
1967
2057
|
return {
|
|
1968
2058
|
cardId: c.cardID,
|
|
1969
2059
|
courseId: c.courseID,
|
|
@@ -1975,7 +2065,7 @@ var init_elo = __esm({
|
|
|
1975
2065
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-ELO-default",
|
|
1976
2066
|
action: "generated",
|
|
1977
2067
|
score: samplingKey,
|
|
1978
|
-
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}),
|
|
2068
|
+
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), relevance ${relevance.toFixed(3)}, key ${samplingKey.toFixed(3)}`
|
|
1979
2069
|
}
|
|
1980
2070
|
]
|
|
1981
2071
|
};
|
|
@@ -2043,7 +2133,7 @@ function shuffleInPlace(arr) {
|
|
|
2043
2133
|
function pickTopByScore(cards, limit) {
|
|
2044
2134
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
2045
2135
|
}
|
|
2046
|
-
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;
|
|
2136
|
+
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;
|
|
2047
2137
|
var init_prescribed = __esm({
|
|
2048
2138
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
2049
2139
|
"use strict";
|
|
@@ -2054,9 +2144,12 @@ var init_prescribed = __esm({
|
|
|
2054
2144
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
2055
2145
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
2056
2146
|
DEFAULT_MIN_COUNT = 3;
|
|
2147
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
2148
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
2057
2149
|
BASE_TARGET_SCORE = 1;
|
|
2058
2150
|
BASE_SUPPORT_SCORE = 0.8;
|
|
2059
2151
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
2152
|
+
BASE_PRACTICE_SCORE = 1;
|
|
2060
2153
|
MAX_TARGET_MULTIPLIER = 8;
|
|
2061
2154
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
2062
2155
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -2164,7 +2257,18 @@ var init_prescribed = __esm({
|
|
|
2164
2257
|
courseId,
|
|
2165
2258
|
emittedIds
|
|
2166
2259
|
);
|
|
2167
|
-
|
|
2260
|
+
const practiceCards = this.buildPracticeCards({
|
|
2261
|
+
group,
|
|
2262
|
+
courseId,
|
|
2263
|
+
emittedIds,
|
|
2264
|
+
cardsByTag,
|
|
2265
|
+
hierarchyConfigs,
|
|
2266
|
+
userTagElo,
|
|
2267
|
+
userGlobalElo,
|
|
2268
|
+
activeIds,
|
|
2269
|
+
seenIds
|
|
2270
|
+
});
|
|
2271
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
2168
2272
|
}
|
|
2169
2273
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2170
2274
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -2192,6 +2296,7 @@ var init_prescribed = __esm({
|
|
|
2192
2296
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
2193
2297
|
for (const card of finalCards) {
|
|
2194
2298
|
const prov = card.provenance[0];
|
|
2299
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
2195
2300
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
2196
2301
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
2197
2302
|
if (!groupId) continue;
|
|
@@ -2261,7 +2366,12 @@ var init_prescribed = __esm({
|
|
|
2261
2366
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
2262
2367
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
2263
2368
|
},
|
|
2264
|
-
retireOnEncounter: raw.retireOnEncounter !== false
|
|
2369
|
+
retireOnEncounter: raw.retireOnEncounter !== false,
|
|
2370
|
+
practiceTagPatterns: dedupe(
|
|
2371
|
+
Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
|
|
2372
|
+
),
|
|
2373
|
+
practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
|
|
2374
|
+
maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
|
|
2265
2375
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
2266
2376
|
return { groups };
|
|
2267
2377
|
} catch {
|
|
@@ -2484,6 +2594,92 @@ var init_prescribed = __esm({
|
|
|
2484
2594
|
}
|
|
2485
2595
|
return cards;
|
|
2486
2596
|
}
|
|
2597
|
+
/**
|
|
2598
|
+
* Emit drill cards for *unlocked-but-under-practiced* skills.
|
|
2599
|
+
*
|
|
2600
|
+
* For each course tag matching the group's `practiceTagPatterns` that is both
|
|
2601
|
+
* unlocked (all hierarchy prerequisites met — i.e. the learner has been
|
|
2602
|
+
* introduced to it) and under-practiced (per-tag attempt count below
|
|
2603
|
+
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2604
|
+
* into the candidate pool. It exists because global-ELO retrieval
|
|
2605
|
+
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2606
|
+
* freshly-introduced skill — putting them in the pool here lets the pipeline's
|
|
2607
|
+
* scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
|
|
2608
|
+
* this method's job; it only guarantees presence.
|
|
2609
|
+
*
|
|
2610
|
+
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2611
|
+
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
2612
|
+
*/
|
|
2613
|
+
buildPracticeCards(args) {
|
|
2614
|
+
const {
|
|
2615
|
+
group,
|
|
2616
|
+
courseId,
|
|
2617
|
+
emittedIds,
|
|
2618
|
+
cardsByTag,
|
|
2619
|
+
hierarchyConfigs,
|
|
2620
|
+
userTagElo,
|
|
2621
|
+
userGlobalElo,
|
|
2622
|
+
activeIds,
|
|
2623
|
+
seenIds
|
|
2624
|
+
} = args;
|
|
2625
|
+
const patterns = group.practiceTagPatterns ?? [];
|
|
2626
|
+
if (patterns.length === 0) return [];
|
|
2627
|
+
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2628
|
+
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2629
|
+
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2630
|
+
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2631
|
+
);
|
|
2632
|
+
if (practiceTags.length === 0) return [];
|
|
2633
|
+
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2634
|
+
supportTags: practiceTags,
|
|
2635
|
+
cardsByTag,
|
|
2636
|
+
activeIds,
|
|
2637
|
+
seenIds,
|
|
2638
|
+
excludedIds: emittedIds,
|
|
2639
|
+
limit: maxPractice
|
|
2640
|
+
});
|
|
2641
|
+
if (practiceCardIds.length === 0) return [];
|
|
2642
|
+
logger.info(
|
|
2643
|
+
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2644
|
+
);
|
|
2645
|
+
const cards = [];
|
|
2646
|
+
for (const cardId of practiceCardIds) {
|
|
2647
|
+
emittedIds.add(cardId);
|
|
2648
|
+
cards.push({
|
|
2649
|
+
cardId,
|
|
2650
|
+
courseId,
|
|
2651
|
+
score: BASE_PRACTICE_SCORE,
|
|
2652
|
+
provenance: [
|
|
2653
|
+
{
|
|
2654
|
+
strategy: "prescribed",
|
|
2655
|
+
strategyName: this.strategyName || this.name,
|
|
2656
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2657
|
+
action: "generated",
|
|
2658
|
+
score: BASE_PRACTICE_SCORE,
|
|
2659
|
+
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2660
|
+
}
|
|
2661
|
+
]
|
|
2662
|
+
});
|
|
2663
|
+
}
|
|
2664
|
+
return cards;
|
|
2665
|
+
}
|
|
2666
|
+
/**
|
|
2667
|
+
* True for a skill that was *gated and is now reached*: it has at least one
|
|
2668
|
+
* declared hierarchy prerequisite set, and every set is fully satisfied by the
|
|
2669
|
+
* learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
|
|
2670
|
+
* — an ungated tag was never "introduced" in the curricular sense, so it isn't
|
|
2671
|
+
* a post-intro drill target (e.g. whole-word spelling tags that share the
|
|
2672
|
+
* `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
|
|
2673
|
+
* ELO retrieval. This is the precise population the retrieval gap strands:
|
|
2674
|
+
* just-unlocked, low-ELO skills.
|
|
2675
|
+
*/
|
|
2676
|
+
isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
|
|
2677
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
2678
|
+
if (prereqSets.length === 0) return false;
|
|
2679
|
+
return prereqSets.every(
|
|
2680
|
+
(prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
2681
|
+
);
|
|
2682
|
+
}
|
|
2487
2683
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2488
2684
|
if (supportTags.length === 0) {
|
|
2489
2685
|
return [];
|
|
@@ -4278,7 +4474,7 @@ function logResultCards(cards) {
|
|
|
4278
4474
|
for (let i = 0; i < cards.length; i++) {
|
|
4279
4475
|
const c = cards[i];
|
|
4280
4476
|
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
4281
|
-
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
4477
|
+
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) => {
|
|
4282
4478
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
4283
4479
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
4284
4480
|
}).join(" | ");
|
|
@@ -4309,6 +4505,7 @@ var init_Pipeline = __esm({
|
|
|
4309
4505
|
init_logger();
|
|
4310
4506
|
init_orchestration();
|
|
4311
4507
|
init_PipelineDebugger();
|
|
4508
|
+
init_diversityRerank();
|
|
4312
4509
|
VERBOSE_RESULTS = true;
|
|
4313
4510
|
Pipeline = class extends ContentNavigator {
|
|
4314
4511
|
generator;
|
|
@@ -4482,6 +4679,7 @@ var init_Pipeline = __esm({
|
|
|
4482
4679
|
this._ephemeralHints = null;
|
|
4483
4680
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
4484
4681
|
}
|
|
4682
|
+
cards = diversityRerank(cards);
|
|
4485
4683
|
cards.sort((a, b) => b.score - a.score);
|
|
4486
4684
|
const tFilter = performance.now();
|
|
4487
4685
|
const result = cards.slice(0, limit);
|
|
@@ -5050,6 +5248,7 @@ var init_3 = __esm({
|
|
|
5050
5248
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
5051
5249
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
5052
5250
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5251
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
5053
5252
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
5054
5253
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
5055
5254
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -5075,9 +5274,12 @@ var init_3 = __esm({
|
|
|
5075
5274
|
var navigators_exports = {};
|
|
5076
5275
|
__export(navigators_exports, {
|
|
5077
5276
|
ContentNavigator: () => ContentNavigator,
|
|
5277
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
5278
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
5078
5279
|
NavigatorRole: () => NavigatorRole,
|
|
5079
5280
|
NavigatorRoles: () => NavigatorRoles,
|
|
5080
5281
|
Navigators: () => Navigators,
|
|
5282
|
+
diversityRerank: () => diversityRerank,
|
|
5081
5283
|
getCardOrigin: () => getCardOrigin,
|
|
5082
5284
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
5083
5285
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -5161,6 +5363,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
5161
5363
|
var init_navigators = __esm({
|
|
5162
5364
|
"src/core/navigators/index.ts"() {
|
|
5163
5365
|
"use strict";
|
|
5366
|
+
init_diversityRerank();
|
|
5164
5367
|
init_PipelineDebugger();
|
|
5165
5368
|
init_logger();
|
|
5166
5369
|
init_();
|
|
@@ -11220,8 +11423,17 @@ var ItemQueue = class {
|
|
|
11220
11423
|
* Merge new items into the front of the queue, skipping duplicates.
|
|
11221
11424
|
* Used by additive replans to inject high-quality candidates without
|
|
11222
11425
|
* discarding the existing queue contents.
|
|
11426
|
+
*
|
|
11427
|
+
* `forceFrontIds` carries the mandatory (`+INF`) cards in this batch — a
|
|
11428
|
+
* durable `requireCard`/`requireTag` re-asserted by every replan. An ordinary
|
|
11429
|
+
* duplicate is left in place (skip), but a mandatory one that's *already*
|
|
11430
|
+
* queued is pulled out of its current slot so it rejoins at the front in batch
|
|
11431
|
+
* order. Without this, an additive merge unshifts fresh non-required cards
|
|
11432
|
+
* ahead of an already-present required card, steadily burying it until it never
|
|
11433
|
+
* gets drawn — defeating the "must appear" guarantee. Returns the count of
|
|
11434
|
+
* genuinely new cards added (re-fronted duplicates are not counted).
|
|
11223
11435
|
*/
|
|
11224
|
-
mergeToFront(items, cardIdExtractor) {
|
|
11436
|
+
mergeToFront(items, cardIdExtractor, forceFrontIds) {
|
|
11225
11437
|
let added = 0;
|
|
11226
11438
|
const toInsert = [];
|
|
11227
11439
|
for (const item of items) {
|
|
@@ -11230,6 +11442,11 @@ var ItemQueue = class {
|
|
|
11230
11442
|
this.seenCardIds.push(cardId);
|
|
11231
11443
|
toInsert.push(item);
|
|
11232
11444
|
added++;
|
|
11445
|
+
} else if (forceFrontIds?.has(cardId)) {
|
|
11446
|
+
const idx = this.q.findIndex((qi) => cardIdExtractor(qi) === cardId);
|
|
11447
|
+
if (idx >= 0) {
|
|
11448
|
+
toInsert.push(...this.q.splice(idx, 1));
|
|
11449
|
+
}
|
|
11233
11450
|
}
|
|
11234
11451
|
}
|
|
11235
11452
|
this.q.unshift(...toInsert);
|
|
@@ -13071,7 +13288,15 @@ var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834"
|
|
|
13071
13288
|
var spinnerFrame = 0;
|
|
13072
13289
|
var overlayEl = null;
|
|
13073
13290
|
var pollHandle = null;
|
|
13074
|
-
var
|
|
13291
|
+
var lastSnapshot = null;
|
|
13292
|
+
var copyFlashUntil = 0;
|
|
13293
|
+
var minified = false;
|
|
13294
|
+
var expanded = {
|
|
13295
|
+
reviewQ: false,
|
|
13296
|
+
newQ: false,
|
|
13297
|
+
failedQ: false,
|
|
13298
|
+
drawn: false
|
|
13299
|
+
};
|
|
13075
13300
|
function toggleSessionOverlay() {
|
|
13076
13301
|
if (typeof document === "undefined") {
|
|
13077
13302
|
logger.info("[Session Overlay] No DOM available (non-browser host); overlay unavailable.");
|
|
@@ -13086,6 +13311,7 @@ function toggleSessionOverlay() {
|
|
|
13086
13311
|
}
|
|
13087
13312
|
}
|
|
13088
13313
|
function mount() {
|
|
13314
|
+
minified = false;
|
|
13089
13315
|
overlayEl = document.createElement("div");
|
|
13090
13316
|
overlayEl.id = OVERLAY_ID;
|
|
13091
13317
|
Object.assign(overlayEl.style, {
|
|
@@ -13124,11 +13350,23 @@ function render() {
|
|
|
13124
13350
|
spinnerFrame++;
|
|
13125
13351
|
const ctrl = getActiveController();
|
|
13126
13352
|
if (!ctrl) {
|
|
13127
|
-
|
|
13353
|
+
lastSnapshot = null;
|
|
13354
|
+
overlayEl.innerHTML = headerHtml() + (minified ? "" : `<div style="opacity:.65">No active session.</div>`);
|
|
13355
|
+
attachHandlers();
|
|
13128
13356
|
return;
|
|
13129
13357
|
}
|
|
13130
13358
|
const s = ctrl.getDebugSnapshot();
|
|
13131
|
-
|
|
13359
|
+
lastSnapshot = s;
|
|
13360
|
+
if (minified) {
|
|
13361
|
+
overlayEl.innerHTML = headerHtml();
|
|
13362
|
+
attachHandlers();
|
|
13363
|
+
return;
|
|
13364
|
+
}
|
|
13365
|
+
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);
|
|
13366
|
+
attachHandlers();
|
|
13367
|
+
}
|
|
13368
|
+
function attachHandlers() {
|
|
13369
|
+
if (!overlayEl) return;
|
|
13132
13370
|
overlayEl.querySelectorAll("[data-q]").forEach((el) => {
|
|
13133
13371
|
el.onclick = () => {
|
|
13134
13372
|
const key = el.dataset.q;
|
|
@@ -13137,9 +13375,45 @@ function render() {
|
|
|
13137
13375
|
render();
|
|
13138
13376
|
};
|
|
13139
13377
|
});
|
|
13378
|
+
const copyBtn = overlayEl.querySelector("[data-copy]");
|
|
13379
|
+
if (copyBtn) {
|
|
13380
|
+
copyBtn.onclick = (ev) => {
|
|
13381
|
+
ev.stopPropagation();
|
|
13382
|
+
copySnapshot();
|
|
13383
|
+
};
|
|
13384
|
+
}
|
|
13385
|
+
const minBtn = overlayEl.querySelector("[data-min]");
|
|
13386
|
+
if (minBtn) {
|
|
13387
|
+
minBtn.onclick = (ev) => {
|
|
13388
|
+
ev.stopPropagation();
|
|
13389
|
+
minified = !minified;
|
|
13390
|
+
render();
|
|
13391
|
+
};
|
|
13392
|
+
}
|
|
13393
|
+
}
|
|
13394
|
+
function copySnapshot() {
|
|
13395
|
+
const text = snapshotToText(lastSnapshot);
|
|
13396
|
+
const flash = () => {
|
|
13397
|
+
copyFlashUntil = Date.now() + 1200;
|
|
13398
|
+
render();
|
|
13399
|
+
};
|
|
13400
|
+
const clip = typeof navigator !== "undefined" ? navigator.clipboard : void 0;
|
|
13401
|
+
if (clip?.writeText) {
|
|
13402
|
+
clip.writeText(text).then(flash, (err) => {
|
|
13403
|
+
logger.warn(`[Session Overlay] Clipboard write failed: ${String(err)}`);
|
|
13404
|
+
});
|
|
13405
|
+
} else {
|
|
13406
|
+
logger.info(`[Session Overlay] Clipboard unavailable; snapshot follows:
|
|
13407
|
+
${text}`);
|
|
13408
|
+
}
|
|
13140
13409
|
}
|
|
13141
13410
|
function headerHtml() {
|
|
13142
|
-
|
|
13411
|
+
const flashing = Date.now() < copyFlashUntil;
|
|
13412
|
+
const btnLabel = flashing ? "\u2713 copied" : "\u2398 copy";
|
|
13413
|
+
const btnColor = flashing ? "#86efac" : "#93c5fd";
|
|
13414
|
+
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>`;
|
|
13415
|
+
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>`;
|
|
13416
|
+
return `<div style="font-weight:600;color:#93c5fd;margin-bottom:${minified ? 0 : 4}px">${copyBtn}${minBtn}\u2699 SessionController</div>`;
|
|
13143
13417
|
}
|
|
13144
13418
|
function replanHtml(s) {
|
|
13145
13419
|
if (!s.replanActive) {
|
|
@@ -13182,22 +13456,98 @@ function hintsHtml(h) {
|
|
|
13182
13456
|
}
|
|
13183
13457
|
function queueHtml(key, label, q) {
|
|
13184
13458
|
const collapsible = q.length > INLINE_THRESHOLD;
|
|
13185
|
-
const isOpen =
|
|
13459
|
+
const isOpen = collapsible && expanded[key];
|
|
13186
13460
|
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13187
13461
|
const drawn = q.dequeueCount ? ` <span style="opacity:.5">drawn ${q.dequeueCount}</span>` : "";
|
|
13188
13462
|
const titleStyle = collapsible ? "cursor:pointer;color:#f9a8d4" : "color:#f9a8d4";
|
|
13189
13463
|
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13190
13464
|
const title = `<div${titleAttr} style="${titleStyle}">${caret}${label}: ${q.length}${drawn}</div>`;
|
|
13191
|
-
|
|
13192
|
-
|
|
13193
|
-
|
|
13194
|
-
|
|
13195
|
-
|
|
13196
|
-
|
|
13197
|
-
|
|
13465
|
+
if (!q.cards.length) {
|
|
13466
|
+
return title + `<div style="margin:1px 0 6px 6px;opacity:.5">empty</div>`;
|
|
13467
|
+
}
|
|
13468
|
+
const shown = isOpen ? q.cards : q.cards.slice(0, INLINE_THRESHOLD);
|
|
13469
|
+
const hiddenCount = q.length - shown.length;
|
|
13470
|
+
const listMarginBottom = collapsible ? 2 : 6;
|
|
13471
|
+
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>`;
|
|
13472
|
+
if (collapsible) {
|
|
13473
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13474
|
+
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
13198
13475
|
}
|
|
13199
13476
|
return title + body;
|
|
13200
13477
|
}
|
|
13478
|
+
function outcomeGlyph(correct) {
|
|
13479
|
+
if (correct === true) return `<span style="color:#86efac">\u2713</span>`;
|
|
13480
|
+
if (correct === false) return `<span style="color:#fca5a5">\u2717</span>`;
|
|
13481
|
+
return `<span style="opacity:.5">\xB7</span>`;
|
|
13482
|
+
}
|
|
13483
|
+
function drawnHtml(key, drawn) {
|
|
13484
|
+
const collapsible = drawn.length > INLINE_THRESHOLD;
|
|
13485
|
+
const isOpen = collapsible && expanded[key];
|
|
13486
|
+
const caret = collapsible ? expanded[key] ? "\u25BE " : "\u25B8 " : "";
|
|
13487
|
+
const titleStyle = collapsible ? "cursor:pointer;color:#c4b5fd" : "color:#c4b5fd";
|
|
13488
|
+
const titleAttr = collapsible ? ` data-q="${key}"` : "";
|
|
13489
|
+
const title = `<div${titleAttr} style="${titleStyle}">${caret}drawn: ${drawn.length}</div>`;
|
|
13490
|
+
if (!drawn.length) {
|
|
13491
|
+
return title + `<div style="margin:1px 0 6px 6px;opacity:.5">none yet</div>`;
|
|
13492
|
+
}
|
|
13493
|
+
const shown = isOpen ? drawn : drawn.slice(0, INLINE_THRESHOLD);
|
|
13494
|
+
const hiddenCount = drawn.length - shown.length;
|
|
13495
|
+
const listMarginBottom = collapsible ? 2 : 6;
|
|
13496
|
+
const rows = shown.map((d) => {
|
|
13497
|
+
const retries = d.attempts > 1 ? `<span style="opacity:.5"> \xD7${d.attempts}</span>` : "";
|
|
13498
|
+
const time = `<span style="opacity:.45"> ${Math.round(d.timeSpentMs / 100) / 10}s</span>`;
|
|
13499
|
+
return `<li style="white-space:nowrap">${outcomeGlyph(d.correct)} ${esc(d.cardID)}<span style="opacity:.5"> [${esc(d.status)}]</span>${retries}${time}</li>`;
|
|
13500
|
+
}).join("");
|
|
13501
|
+
let body = `<ol style="margin:2px 0 ${listMarginBottom}px 0;padding-left:20px">${rows}</ol>`;
|
|
13502
|
+
if (collapsible) {
|
|
13503
|
+
const footer = isOpen ? "\u25BE show less" : `\u2026 +${hiddenCount} more`;
|
|
13504
|
+
body += `<div data-q="${key}" style="cursor:pointer;margin:0 0 6px 20px;opacity:.6">${footer}</div>`;
|
|
13505
|
+
}
|
|
13506
|
+
return title + body;
|
|
13507
|
+
}
|
|
13508
|
+
function snapshotToText(s) {
|
|
13509
|
+
if (!s) return "SessionController \u2014 no active session.";
|
|
13510
|
+
const lines = [];
|
|
13511
|
+
lines.push("=== SessionController ===");
|
|
13512
|
+
lines.push(`time ${formatTime(s.secondsRemaining)}`);
|
|
13513
|
+
if (s.hasCardGuarantee) lines.push(`guarantee: ${s.minCardsGuarantee}`);
|
|
13514
|
+
lines.push(`well-indicated left: ${s.wellIndicatedRemaining}`);
|
|
13515
|
+
lines.push(`current: ${s.currentCard ?? "\u2014"}`);
|
|
13516
|
+
lines.push(
|
|
13517
|
+
s.replanActive ? `replan: ACTIVE [${s.replanLabel ?? "(auto)"}]` : "replan: idle"
|
|
13518
|
+
);
|
|
13519
|
+
lines.push("");
|
|
13520
|
+
lines.push("sessionHints:");
|
|
13521
|
+
const h = s.sessionHints;
|
|
13522
|
+
const hintParts = [];
|
|
13523
|
+
if (h) {
|
|
13524
|
+
if (h.boostTags && Object.keys(h.boostTags).length)
|
|
13525
|
+
hintParts.push(` boost: ${Object.entries(h.boostTags).map(([k, v]) => `${k}\xD7${v}`).join(", ")}`);
|
|
13526
|
+
if (h.boostCards && Object.keys(h.boostCards).length)
|
|
13527
|
+
hintParts.push(` boostCards: ${Object.entries(h.boostCards).map(([k, v]) => `${k}\xD7${v}`).join(", ")}`);
|
|
13528
|
+
if (h.requireCards?.length) hintParts.push(` require: ${h.requireCards.join(", ")}`);
|
|
13529
|
+
if (h.requireTags?.length) hintParts.push(` requireTags: ${h.requireTags.join(", ")}`);
|
|
13530
|
+
if (h.excludeTags?.length) hintParts.push(` exclude: ${h.excludeTags.join(", ")}`);
|
|
13531
|
+
if (h.excludeCards?.length) hintParts.push(` excludeCards: ${h.excludeCards.join(", ")}`);
|
|
13532
|
+
}
|
|
13533
|
+
lines.push(hintParts.length ? hintParts.join("\n") : " none");
|
|
13534
|
+
const queueText = (label, q) => {
|
|
13535
|
+
lines.push("");
|
|
13536
|
+
lines.push(`${label}: ${q.length} (drawn ${q.dequeueCount})`);
|
|
13537
|
+
q.cards.forEach((c, i) => lines.push(` ${i + 1}. ${c}`));
|
|
13538
|
+
};
|
|
13539
|
+
queueText("reviewQ", s.reviewQ);
|
|
13540
|
+
queueText("newQ", s.newQ);
|
|
13541
|
+
queueText("failedQ", s.failedQ);
|
|
13542
|
+
lines.push("");
|
|
13543
|
+
lines.push(`drawn: ${s.drawnCards.length}`);
|
|
13544
|
+
s.drawnCards.forEach((d, i) => {
|
|
13545
|
+
const mark = d.correct === true ? "\u2713" : d.correct === false ? "\u2717" : "\xB7";
|
|
13546
|
+
const time = `${Math.round(d.timeSpentMs / 100) / 10}s`;
|
|
13547
|
+
lines.push(` ${i + 1}. ${mark} ${d.cardID} [${d.status}] \xD7${d.attempts} ${time}`);
|
|
13548
|
+
});
|
|
13549
|
+
return lines.join("\n");
|
|
13550
|
+
}
|
|
13201
13551
|
function formatTime(totalSeconds) {
|
|
13202
13552
|
const s = Math.max(0, Math.round(totalSeconds));
|
|
13203
13553
|
const m = Math.floor(s / 60);
|
|
@@ -13567,6 +13917,21 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13567
13917
|
* recomputed per-run in `_runReplan` and would otherwise go stale.
|
|
13568
13918
|
*/
|
|
13569
13919
|
_sessionHints = null;
|
|
13920
|
+
/**
|
|
13921
|
+
* Card IDs that have been *served* (drawn/consumed) this session. Populated
|
|
13922
|
+
* at the single consumption choke-point (removeItemFromQueue), so it reflects
|
|
13923
|
+
* a draw the instant it happens — earlier than `_sessionRecord`, which only
|
|
13924
|
+
* lands once the card is *responded to*.
|
|
13925
|
+
*
|
|
13926
|
+
* Used to keep already-served cards out of newQ on every (re)plan: a `new`
|
|
13927
|
+
* card shown once must never re-enter newQ this session. This is the general
|
|
13928
|
+
* guard against re-presentation — including the case where a replan in flight
|
|
13929
|
+
* captured a now-drawn card (e.g. a +INF require-injected follow-up the
|
|
13930
|
+
* depletion prefetch grabbed just before it was drawn). Reviews/failed cards
|
|
13931
|
+
* legitimately recur and are tracked by their own queues, so this only gates
|
|
13932
|
+
* `new`-origin candidates.
|
|
13933
|
+
*/
|
|
13934
|
+
_servedCardIds = /* @__PURE__ */ new Set();
|
|
13570
13935
|
/**
|
|
13571
13936
|
* Consumer-supplied hooks invoked after each question response is processed.
|
|
13572
13937
|
* Seeded from constructor options (threaded from
|
|
@@ -13776,6 +14141,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13776
14141
|
if (opts.mode && opts.mode !== "replace") return true;
|
|
13777
14142
|
if (opts.hints && Object.keys(opts.hints).length > 0) return true;
|
|
13778
14143
|
if (opts.sessionHints !== void 0) return true;
|
|
14144
|
+
if (opts.mergeSessionHints !== void 0) return true;
|
|
13779
14145
|
return false;
|
|
13780
14146
|
}
|
|
13781
14147
|
/**
|
|
@@ -13811,6 +14177,10 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13811
14177
|
`[Replan] Session hints ${opts.sessionHints ? "set" : "cleared"}: ${JSON.stringify(opts.sessionHints)}`
|
|
13812
14178
|
);
|
|
13813
14179
|
}
|
|
14180
|
+
if (opts.mergeSessionHints !== void 0) {
|
|
14181
|
+
this._sessionHints = mergeHints2([this._sessionHints, opts.mergeSessionHints]) ?? null;
|
|
14182
|
+
this.log(`[Replan] Session hints merged: ${JSON.stringify(this._sessionHints)}`);
|
|
14183
|
+
}
|
|
13814
14184
|
this._applyHintsToSources(opts.hints, opts.label);
|
|
13815
14185
|
const labelTag = opts.label ? ` [${opts.label}]` : "";
|
|
13816
14186
|
this.log(
|
|
@@ -13863,6 +14233,16 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13863
14233
|
}
|
|
13864
14234
|
return { length: q.length, dequeueCount: q.dequeueCount, cards };
|
|
13865
14235
|
};
|
|
14236
|
+
const drawnCards = this._sessionRecord.map((r) => {
|
|
14237
|
+
const last = r.records[r.records.length - 1];
|
|
14238
|
+
return {
|
|
14239
|
+
cardID: r.item.cardID,
|
|
14240
|
+
status: r.item.status,
|
|
14241
|
+
attempts: r.records.length,
|
|
14242
|
+
correct: last && isQuestionRecord(last) ? last.isCorrect : null,
|
|
14243
|
+
timeSpentMs: r.records.reduce((sum, rec) => sum + rec.timeSpent, 0)
|
|
14244
|
+
};
|
|
14245
|
+
});
|
|
13866
14246
|
return {
|
|
13867
14247
|
secondsRemaining: this.secondsRemaining,
|
|
13868
14248
|
hasCardGuarantee: this.hasCardGuarantee,
|
|
@@ -13874,7 +14254,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
13874
14254
|
replanLabel: this._activeReplanLabel,
|
|
13875
14255
|
reviewQ: describe(this.reviewQ),
|
|
13876
14256
|
newQ: describe(this.newQ),
|
|
13877
|
-
failedQ: describe(this.failedQ)
|
|
14257
|
+
failedQ: describe(this.failedQ),
|
|
14258
|
+
drawnCards
|
|
13878
14259
|
};
|
|
13879
14260
|
}
|
|
13880
14261
|
/**
|
|
@@ -14183,7 +14564,16 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14183
14564
|
mixedWeighted
|
|
14184
14565
|
);
|
|
14185
14566
|
const reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review").slice(0, this._initialReviewCap);
|
|
14186
|
-
const
|
|
14567
|
+
const newCandidates = mixedWeighted.filter(
|
|
14568
|
+
(w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)
|
|
14569
|
+
);
|
|
14570
|
+
const mandatoryWeighted = newCandidates.filter((w) => w.score === Number.POSITIVE_INFINITY);
|
|
14571
|
+
const optionalWeighted = newCandidates.filter((w) => w.score !== Number.POSITIVE_INFINITY);
|
|
14572
|
+
const newWeighted = [
|
|
14573
|
+
...mandatoryWeighted,
|
|
14574
|
+
...optionalWeighted.slice(0, Math.max(0, newLimit - mandatoryWeighted.length))
|
|
14575
|
+
];
|
|
14576
|
+
const mandatoryIds = new Set(mandatoryWeighted.map((w) => w.cardId));
|
|
14187
14577
|
logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
|
|
14188
14578
|
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
14189
14579
|
if (!replan) {
|
|
@@ -14218,7 +14608,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14218
14608
|
`;
|
|
14219
14609
|
}
|
|
14220
14610
|
if (additive) {
|
|
14221
|
-
const added = this.newQ.mergeToFront(newItems, (item) => item.cardID);
|
|
14611
|
+
const added = this.newQ.mergeToFront(newItems, (item) => item.cardID, mandatoryIds);
|
|
14222
14612
|
report += `Additive merge: ${added} new cards added to front of newQ
|
|
14223
14613
|
`;
|
|
14224
14614
|
} else if (replan) {
|
|
@@ -14320,7 +14710,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14320
14710
|
this.log(
|
|
14321
14711
|
`[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
|
|
14322
14712
|
);
|
|
14323
|
-
void this.requestReplan({ label: "auto:depletion" });
|
|
14713
|
+
void this.requestReplan({ label: "auto:depletion", mode: "merge" });
|
|
14324
14714
|
}
|
|
14325
14715
|
const REPLAN_BUFFER = 3;
|
|
14326
14716
|
if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
|
|
@@ -14461,6 +14851,8 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14461
14851
|
* Remove an item from its source queue after consumption by nextCard().
|
|
14462
14852
|
*/
|
|
14463
14853
|
removeItemFromQueue(item) {
|
|
14854
|
+
this._clearDurableRequirement(item.cardID);
|
|
14855
|
+
this._servedCardIds.add(item.cardID);
|
|
14464
14856
|
if (this.reviewQ.peek(0)?.cardID === item.cardID) {
|
|
14465
14857
|
this.reviewQ.dequeue((queueItem) => queueItem.cardID);
|
|
14466
14858
|
} else if (this.newQ.peek(0)?.cardID === item.cardID) {
|
|
@@ -14472,6 +14864,27 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14472
14864
|
this.failedQ.dequeue((queueItem) => queueItem.cardID);
|
|
14473
14865
|
}
|
|
14474
14866
|
}
|
|
14867
|
+
/**
|
|
14868
|
+
* Remove a satisfied card ID from the durable session-hint `requireCards`
|
|
14869
|
+
* list. Called when a card is consumed (see removeItemFromQueue). No-op if
|
|
14870
|
+
* the card was not a durable requirement.
|
|
14871
|
+
*
|
|
14872
|
+
* Matches literal IDs only: a glob/pattern requirement (which may stand for
|
|
14873
|
+
* several cards) is NOT considered satisfied by a single draw and is left in
|
|
14874
|
+
* place — durable patterns are the caller's responsibility, one-shot `hints`
|
|
14875
|
+
* remain the right tool for them.
|
|
14876
|
+
*/
|
|
14877
|
+
_clearDurableRequirement(cardID) {
|
|
14878
|
+
const req = this._sessionHints?.requireCards;
|
|
14879
|
+
if (!req || req.length === 0) return;
|
|
14880
|
+
const next = req.filter((id) => id !== cardID);
|
|
14881
|
+
if (next.length === req.length) return;
|
|
14882
|
+
this._sessionHints = {
|
|
14883
|
+
...this._sessionHints,
|
|
14884
|
+
requireCards: next.length > 0 ? next : void 0
|
|
14885
|
+
};
|
|
14886
|
+
this.log(`[Replan] Durable requirement satisfied & cleared on draw: ${cardID}`);
|
|
14887
|
+
}
|
|
14475
14888
|
/**
|
|
14476
14889
|
* End the session and record learning outcomes.
|
|
14477
14890
|
*
|
|
@@ -14526,6 +14939,8 @@ export {
|
|
|
14526
14939
|
ContentNavigator,
|
|
14527
14940
|
CouchDBToStaticPacker,
|
|
14528
14941
|
CourseLookup,
|
|
14942
|
+
DIVERSITY_FLOOR,
|
|
14943
|
+
DIVERSITY_STRENGTH,
|
|
14529
14944
|
DocType,
|
|
14530
14945
|
DocTypePrefixes,
|
|
14531
14946
|
ENV,
|
|
@@ -14551,6 +14966,7 @@ export {
|
|
|
14551
14966
|
computeSpread,
|
|
14552
14967
|
computeStrategyGradient,
|
|
14553
14968
|
createOrchestrationContext,
|
|
14969
|
+
diversityRerank,
|
|
14554
14970
|
docIsDeleted,
|
|
14555
14971
|
endSessionTracking,
|
|
14556
14972
|
ensureAppDataDirectory,
|