@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
package/dist/index.d.cts
CHANGED
|
@@ -4,7 +4,7 @@ import { D as DataLayerProvider } from './dataLayerProvider-CiA2Rr0v.cjs';
|
|
|
4
4
|
import { C as CardHistory, c as CardRecord, d as QuestionRecord } from './types-legacy-4tlwHnXo.cjs';
|
|
5
5
|
export { e as CardData, f as CourseListData, h as DataShapeData, g as DisplayableData, D as DocType, b as DocTypePrefixes, F as Field, G as GuestUsername, Q as QualifiedCardID, i as QuestionData, S as SkuilderCourseData, a as Tag, T as TagStub, l as log } from './types-legacy-4tlwHnXo.cjs';
|
|
6
6
|
import { Loggable } from './core/index.cjs';
|
|
7
|
-
export { BulkCardProcessorConfig, CardFilter, CardFilterFactory, FilterContext, FilterImpact, GeneratorSummary, GradientObservation, GradientResult, ImportResult, PeriodUpdateInput, PeriodUpdateResult, PipelineRunReport, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig } from './core/index.cjs';
|
|
7
|
+
export { BulkCardProcessorConfig, CardFilter, CardFilterFactory, DIVERSITY_FLOOR, DIVERSITY_STRENGTH, DiversityRerankOptions, FilterContext, FilterImpact, GeneratorSummary, GradientObservation, GradientResult, ImportResult, PeriodUpdateInput, PeriodUpdateResult, PipelineRunReport, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, diversityRerank, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig } from './core/index.cjs';
|
|
8
8
|
import { TaggedPerformance, TagFilter, DataShape, CourseConfig } from '@vue-skuilder/common';
|
|
9
9
|
import { S as StaticCourseManifest } from './types-BFUa1pa3.cjs';
|
|
10
10
|
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig } from './types-BFUa1pa3.cjs';
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { D as DataLayerProvider } from './dataLayerProvider-DrBqOUa3.js';
|
|
|
4
4
|
import { C as CardHistory, c as CardRecord, d as QuestionRecord } from './types-legacy-4tlwHnXo.js';
|
|
5
5
|
export { e as CardData, f as CourseListData, h as DataShapeData, g as DisplayableData, D as DocType, b as DocTypePrefixes, F as Field, G as GuestUsername, Q as QualifiedCardID, i as QuestionData, S as SkuilderCourseData, a as Tag, T as TagStub, l as log } from './types-legacy-4tlwHnXo.js';
|
|
6
6
|
import { Loggable } from './core/index.js';
|
|
7
|
-
export { BulkCardProcessorConfig, CardFilter, CardFilterFactory, FilterContext, FilterImpact, GeneratorSummary, GradientObservation, GradientResult, ImportResult, PeriodUpdateInput, PeriodUpdateResult, PipelineRunReport, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig } from './core/index.js';
|
|
7
|
+
export { BulkCardProcessorConfig, CardFilter, CardFilterFactory, DIVERSITY_FLOOR, DIVERSITY_STRENGTH, DiversityRerankOptions, FilterContext, FilterImpact, GeneratorSummary, GradientObservation, GradientResult, ImportResult, PeriodUpdateInput, PeriodUpdateResult, PipelineRunReport, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, diversityRerank, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig } from './core/index.js';
|
|
8
8
|
import { TaggedPerformance, TagFilter, DataShape, CourseConfig } from '@vue-skuilder/common';
|
|
9
9
|
import { S as StaticCourseManifest } from './types-CHgpWQAY.js';
|
|
10
10
|
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig } from './types-CHgpWQAY.js';
|
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, {
|
|
@@ -2067,7 +2156,7 @@ function shuffleInPlace(arr) {
|
|
|
2067
2156
|
function pickTopByScore(cards, limit) {
|
|
2068
2157
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
2069
2158
|
}
|
|
2070
|
-
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;
|
|
2071
2160
|
var init_prescribed = __esm({
|
|
2072
2161
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
2073
2162
|
"use strict";
|
|
@@ -2078,9 +2167,12 @@ var init_prescribed = __esm({
|
|
|
2078
2167
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
2079
2168
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
2080
2169
|
DEFAULT_MIN_COUNT = 3;
|
|
2170
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
2171
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
2081
2172
|
BASE_TARGET_SCORE = 1;
|
|
2082
2173
|
BASE_SUPPORT_SCORE = 0.8;
|
|
2083
2174
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
2175
|
+
BASE_PRACTICE_SCORE = 1;
|
|
2084
2176
|
MAX_TARGET_MULTIPLIER = 8;
|
|
2085
2177
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
2086
2178
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -2188,7 +2280,18 @@ var init_prescribed = __esm({
|
|
|
2188
2280
|
courseId,
|
|
2189
2281
|
emittedIds
|
|
2190
2282
|
);
|
|
2191
|
-
|
|
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);
|
|
2192
2295
|
}
|
|
2193
2296
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2194
2297
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -2216,6 +2319,7 @@ var init_prescribed = __esm({
|
|
|
2216
2319
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
2217
2320
|
for (const card of finalCards) {
|
|
2218
2321
|
const prov = card.provenance[0];
|
|
2322
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
2219
2323
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
2220
2324
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
2221
2325
|
if (!groupId) continue;
|
|
@@ -2285,7 +2389,12 @@ var init_prescribed = __esm({
|
|
|
2285
2389
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
2286
2390
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
2287
2391
|
},
|
|
2288
|
-
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
|
|
2289
2398
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
2290
2399
|
return { groups };
|
|
2291
2400
|
} catch {
|
|
@@ -2508,6 +2617,92 @@ var init_prescribed = __esm({
|
|
|
2508
2617
|
}
|
|
2509
2618
|
return cards;
|
|
2510
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
|
+
}
|
|
2511
2706
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2512
2707
|
if (supportTags.length === 0) {
|
|
2513
2708
|
return [];
|
|
@@ -4301,7 +4496,7 @@ function logResultCards(cards) {
|
|
|
4301
4496
|
for (let i = 0; i < cards.length; i++) {
|
|
4302
4497
|
const c = cards[i];
|
|
4303
4498
|
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
4304
|
-
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) => {
|
|
4305
4500
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
4306
4501
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
4307
4502
|
}).join(" | ");
|
|
@@ -4333,6 +4528,7 @@ var init_Pipeline = __esm({
|
|
|
4333
4528
|
init_logger();
|
|
4334
4529
|
init_orchestration();
|
|
4335
4530
|
init_PipelineDebugger();
|
|
4531
|
+
init_diversityRerank();
|
|
4336
4532
|
VERBOSE_RESULTS = true;
|
|
4337
4533
|
Pipeline = class extends ContentNavigator {
|
|
4338
4534
|
generator;
|
|
@@ -4506,6 +4702,7 @@ var init_Pipeline = __esm({
|
|
|
4506
4702
|
this._ephemeralHints = null;
|
|
4507
4703
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
4508
4704
|
}
|
|
4705
|
+
cards = diversityRerank(cards);
|
|
4509
4706
|
cards.sort((a, b) => b.score - a.score);
|
|
4510
4707
|
const tFilter = performance.now();
|
|
4511
4708
|
const result = cards.slice(0, limit);
|
|
@@ -5074,6 +5271,7 @@ var init_3 = __esm({
|
|
|
5074
5271
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
5075
5272
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
5076
5273
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5274
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
5077
5275
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
5078
5276
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
5079
5277
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -5099,9 +5297,12 @@ var init_3 = __esm({
|
|
|
5099
5297
|
var navigators_exports = {};
|
|
5100
5298
|
__export(navigators_exports, {
|
|
5101
5299
|
ContentNavigator: () => ContentNavigator,
|
|
5300
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
5301
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
5102
5302
|
NavigatorRole: () => NavigatorRole,
|
|
5103
5303
|
NavigatorRoles: () => NavigatorRoles,
|
|
5104
5304
|
Navigators: () => Navigators,
|
|
5305
|
+
diversityRerank: () => diversityRerank,
|
|
5105
5306
|
getCardOrigin: () => getCardOrigin,
|
|
5106
5307
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
5107
5308
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -5185,6 +5386,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
5185
5386
|
var init_navigators = __esm({
|
|
5186
5387
|
"src/core/navigators/index.ts"() {
|
|
5187
5388
|
"use strict";
|
|
5389
|
+
init_diversityRerank();
|
|
5188
5390
|
init_PipelineDebugger();
|
|
5189
5391
|
init_logger();
|
|
5190
5392
|
init_();
|
|
@@ -10187,6 +10389,8 @@ __export(index_exports, {
|
|
|
10187
10389
|
ContentNavigator: () => ContentNavigator,
|
|
10188
10390
|
CouchDBToStaticPacker: () => CouchDBToStaticPacker,
|
|
10189
10391
|
CourseLookup: () => CourseLookup,
|
|
10392
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
10393
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
10190
10394
|
DocType: () => DocType,
|
|
10191
10395
|
DocTypePrefixes: () => DocTypePrefixes,
|
|
10192
10396
|
ENV: () => ENV,
|
|
@@ -10212,6 +10416,7 @@ __export(index_exports, {
|
|
|
10212
10416
|
computeSpread: () => computeSpread,
|
|
10213
10417
|
computeStrategyGradient: () => computeStrategyGradient,
|
|
10214
10418
|
createOrchestrationContext: () => createOrchestrationContext,
|
|
10419
|
+
diversityRerank: () => diversityRerank,
|
|
10215
10420
|
docIsDeleted: () => docIsDeleted,
|
|
10216
10421
|
endSessionTracking: () => endSessionTracking,
|
|
10217
10422
|
ensureAppDataDirectory: () => ensureAppDataDirectory,
|
|
@@ -11323,8 +11528,17 @@ var ItemQueue = class {
|
|
|
11323
11528
|
* Merge new items into the front of the queue, skipping duplicates.
|
|
11324
11529
|
* Used by additive replans to inject high-quality candidates without
|
|
11325
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).
|
|
11326
11540
|
*/
|
|
11327
|
-
mergeToFront(items, cardIdExtractor) {
|
|
11541
|
+
mergeToFront(items, cardIdExtractor, forceFrontIds) {
|
|
11328
11542
|
let added = 0;
|
|
11329
11543
|
const toInsert = [];
|
|
11330
11544
|
for (const item of items) {
|
|
@@ -11333,6 +11547,11 @@ var ItemQueue = class {
|
|
|
11333
11547
|
this.seenCardIds.push(cardId);
|
|
11334
11548
|
toInsert.push(item);
|
|
11335
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
|
+
}
|
|
11336
11555
|
}
|
|
11337
11556
|
}
|
|
11338
11557
|
this.q.unshift(...toInsert);
|
|
@@ -14450,7 +14669,16 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14450
14669
|
mixedWeighted
|
|
14451
14670
|
);
|
|
14452
14671
|
const reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review").slice(0, this._initialReviewCap);
|
|
14453
|
-
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));
|
|
14454
14682
|
logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
|
|
14455
14683
|
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
14456
14684
|
if (!replan) {
|
|
@@ -14485,7 +14713,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14485
14713
|
`;
|
|
14486
14714
|
}
|
|
14487
14715
|
if (additive) {
|
|
14488
|
-
const added = this.newQ.mergeToFront(newItems, (item) => item.cardID);
|
|
14716
|
+
const added = this.newQ.mergeToFront(newItems, (item) => item.cardID, mandatoryIds);
|
|
14489
14717
|
report += `Additive merge: ${added} new cards added to front of newQ
|
|
14490
14718
|
`;
|
|
14491
14719
|
} else if (replan) {
|
|
@@ -14817,6 +15045,8 @@ init_factory();
|
|
|
14817
15045
|
ContentNavigator,
|
|
14818
15046
|
CouchDBToStaticPacker,
|
|
14819
15047
|
CourseLookup,
|
|
15048
|
+
DIVERSITY_FLOOR,
|
|
15049
|
+
DIVERSITY_STRENGTH,
|
|
14820
15050
|
DocType,
|
|
14821
15051
|
DocTypePrefixes,
|
|
14822
15052
|
ENV,
|
|
@@ -14842,6 +15072,7 @@ init_factory();
|
|
|
14842
15072
|
computeSpread,
|
|
14843
15073
|
computeStrategyGradient,
|
|
14844
15074
|
createOrchestrationContext,
|
|
15075
|
+
diversityRerank,
|
|
14845
15076
|
docIsDeleted,
|
|
14846
15077
|
endSessionTracking,
|
|
14847
15078
|
ensureAppDataDirectory,
|