@vue-skuilder/db 0.2.5 → 0.2.8
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 +75 -1
- package/dist/core/index.d.ts +75 -1
- package/dist/core/index.js +281 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +277 -4
- package/dist/core/index.mjs.map +1 -1
- package/dist/impl/couch/index.js +273 -4
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +273 -4
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.js +273 -4
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +273 -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 +307 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +303 -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 +103 -2
- package/src/core/navigators/PipelineDebugger.ts +11 -1
- package/src/core/navigators/diversityRerank.ts +185 -0
- package/src/core/navigators/generators/prescribed.ts +173 -1
- package/src/core/navigators/index.ts +12 -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, PipelineForecaster, PipelineRunReport, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, diversityRerank, docIsDeleted, getActivePipeline, 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, PipelineForecaster, PipelineRunReport, SignalConfig, StrategyLearningState, StrategyStateDoc, StrategyStateId, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, diversityRerank, docIsDeleted, getActivePipeline, 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,12 +872,102 @@ 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, {
|
|
878
967
|
buildRunReport: () => buildRunReport,
|
|
879
968
|
captureRun: () => captureRun,
|
|
880
969
|
clearRunHistory: () => clearRunHistory,
|
|
970
|
+
getActivePipeline: () => getActivePipeline,
|
|
881
971
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
882
972
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
883
973
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -885,6 +975,9 @@ __export(PipelineDebugger_exports, {
|
|
|
885
975
|
function registerPipelineForDebug(pipeline) {
|
|
886
976
|
_activePipeline = pipeline;
|
|
887
977
|
}
|
|
978
|
+
function getActivePipeline() {
|
|
979
|
+
return _activePipeline;
|
|
980
|
+
}
|
|
888
981
|
function clearRunHistory() {
|
|
889
982
|
runHistory.length = 0;
|
|
890
983
|
}
|
|
@@ -2067,7 +2160,7 @@ function shuffleInPlace(arr) {
|
|
|
2067
2160
|
function pickTopByScore(cards, limit) {
|
|
2068
2161
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
2069
2162
|
}
|
|
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;
|
|
2163
|
+
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
2164
|
var init_prescribed = __esm({
|
|
2072
2165
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
2073
2166
|
"use strict";
|
|
@@ -2078,9 +2171,12 @@ var init_prescribed = __esm({
|
|
|
2078
2171
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
2079
2172
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
2080
2173
|
DEFAULT_MIN_COUNT = 3;
|
|
2174
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
2175
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
2081
2176
|
BASE_TARGET_SCORE = 1;
|
|
2082
2177
|
BASE_SUPPORT_SCORE = 0.8;
|
|
2083
2178
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
2179
|
+
BASE_PRACTICE_SCORE = 1;
|
|
2084
2180
|
MAX_TARGET_MULTIPLIER = 8;
|
|
2085
2181
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
2086
2182
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -2188,7 +2284,18 @@ var init_prescribed = __esm({
|
|
|
2188
2284
|
courseId,
|
|
2189
2285
|
emittedIds
|
|
2190
2286
|
);
|
|
2191
|
-
|
|
2287
|
+
const practiceCards = this.buildPracticeCards({
|
|
2288
|
+
group,
|
|
2289
|
+
courseId,
|
|
2290
|
+
emittedIds,
|
|
2291
|
+
cardsByTag,
|
|
2292
|
+
hierarchyConfigs,
|
|
2293
|
+
userTagElo,
|
|
2294
|
+
userGlobalElo,
|
|
2295
|
+
activeIds,
|
|
2296
|
+
seenIds
|
|
2297
|
+
});
|
|
2298
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
2192
2299
|
}
|
|
2193
2300
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2194
2301
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -2216,6 +2323,7 @@ var init_prescribed = __esm({
|
|
|
2216
2323
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
2217
2324
|
for (const card of finalCards) {
|
|
2218
2325
|
const prov = card.provenance[0];
|
|
2326
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
2219
2327
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
2220
2328
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
2221
2329
|
if (!groupId) continue;
|
|
@@ -2285,7 +2393,12 @@ var init_prescribed = __esm({
|
|
|
2285
2393
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
2286
2394
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
2287
2395
|
},
|
|
2288
|
-
retireOnEncounter: raw.retireOnEncounter !== false
|
|
2396
|
+
retireOnEncounter: raw.retireOnEncounter !== false,
|
|
2397
|
+
practiceTagPatterns: dedupe(
|
|
2398
|
+
Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
|
|
2399
|
+
),
|
|
2400
|
+
practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
|
|
2401
|
+
maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
|
|
2289
2402
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
2290
2403
|
return { groups };
|
|
2291
2404
|
} catch {
|
|
@@ -2508,6 +2621,92 @@ var init_prescribed = __esm({
|
|
|
2508
2621
|
}
|
|
2509
2622
|
return cards;
|
|
2510
2623
|
}
|
|
2624
|
+
/**
|
|
2625
|
+
* Emit drill cards for *unlocked-but-under-practiced* skills.
|
|
2626
|
+
*
|
|
2627
|
+
* For each course tag matching the group's `practiceTagPatterns` that is both
|
|
2628
|
+
* unlocked (all hierarchy prerequisites met — i.e. the learner has been
|
|
2629
|
+
* introduced to it) and under-practiced (per-tag attempt count below
|
|
2630
|
+
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2631
|
+
* into the candidate pool. It exists because global-ELO retrieval
|
|
2632
|
+
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2633
|
+
* freshly-introduced skill — putting them in the pool here lets the pipeline's
|
|
2634
|
+
* scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
|
|
2635
|
+
* this method's job; it only guarantees presence.
|
|
2636
|
+
*
|
|
2637
|
+
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2638
|
+
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
2639
|
+
*/
|
|
2640
|
+
buildPracticeCards(args) {
|
|
2641
|
+
const {
|
|
2642
|
+
group,
|
|
2643
|
+
courseId,
|
|
2644
|
+
emittedIds,
|
|
2645
|
+
cardsByTag,
|
|
2646
|
+
hierarchyConfigs,
|
|
2647
|
+
userTagElo,
|
|
2648
|
+
userGlobalElo,
|
|
2649
|
+
activeIds,
|
|
2650
|
+
seenIds
|
|
2651
|
+
} = args;
|
|
2652
|
+
const patterns = group.practiceTagPatterns ?? [];
|
|
2653
|
+
if (patterns.length === 0) return [];
|
|
2654
|
+
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2655
|
+
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2656
|
+
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2657
|
+
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2658
|
+
);
|
|
2659
|
+
if (practiceTags.length === 0) return [];
|
|
2660
|
+
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2661
|
+
supportTags: practiceTags,
|
|
2662
|
+
cardsByTag,
|
|
2663
|
+
activeIds,
|
|
2664
|
+
seenIds,
|
|
2665
|
+
excludedIds: emittedIds,
|
|
2666
|
+
limit: maxPractice
|
|
2667
|
+
});
|
|
2668
|
+
if (practiceCardIds.length === 0) return [];
|
|
2669
|
+
logger.info(
|
|
2670
|
+
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2671
|
+
);
|
|
2672
|
+
const cards = [];
|
|
2673
|
+
for (const cardId of practiceCardIds) {
|
|
2674
|
+
emittedIds.add(cardId);
|
|
2675
|
+
cards.push({
|
|
2676
|
+
cardId,
|
|
2677
|
+
courseId,
|
|
2678
|
+
score: BASE_PRACTICE_SCORE,
|
|
2679
|
+
provenance: [
|
|
2680
|
+
{
|
|
2681
|
+
strategy: "prescribed",
|
|
2682
|
+
strategyName: this.strategyName || this.name,
|
|
2683
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2684
|
+
action: "generated",
|
|
2685
|
+
score: BASE_PRACTICE_SCORE,
|
|
2686
|
+
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2687
|
+
}
|
|
2688
|
+
]
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2691
|
+
return cards;
|
|
2692
|
+
}
|
|
2693
|
+
/**
|
|
2694
|
+
* True for a skill that was *gated and is now reached*: it has at least one
|
|
2695
|
+
* declared hierarchy prerequisite set, and every set is fully satisfied by the
|
|
2696
|
+
* learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
|
|
2697
|
+
* — an ungated tag was never "introduced" in the curricular sense, so it isn't
|
|
2698
|
+
* a post-intro drill target (e.g. whole-word spelling tags that share the
|
|
2699
|
+
* `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
|
|
2700
|
+
* ELO retrieval. This is the precise population the retrieval gap strands:
|
|
2701
|
+
* just-unlocked, low-ELO skills.
|
|
2702
|
+
*/
|
|
2703
|
+
isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
|
|
2704
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
2705
|
+
if (prereqSets.length === 0) return false;
|
|
2706
|
+
return prereqSets.every(
|
|
2707
|
+
(prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
2708
|
+
);
|
|
2709
|
+
}
|
|
2511
2710
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2512
2711
|
if (supportTags.length === 0) {
|
|
2513
2712
|
return [];
|
|
@@ -4301,7 +4500,7 @@ function logResultCards(cards) {
|
|
|
4301
4500
|
for (let i = 0; i < cards.length; i++) {
|
|
4302
4501
|
const c = cards[i];
|
|
4303
4502
|
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) => {
|
|
4503
|
+
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
4504
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
4306
4505
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
4307
4506
|
}).join(" | ");
|
|
@@ -4333,6 +4532,7 @@ var init_Pipeline = __esm({
|
|
|
4333
4532
|
init_logger();
|
|
4334
4533
|
init_orchestration();
|
|
4335
4534
|
init_PipelineDebugger();
|
|
4535
|
+
init_diversityRerank();
|
|
4336
4536
|
VERBOSE_RESULTS = true;
|
|
4337
4537
|
Pipeline = class extends ContentNavigator {
|
|
4338
4538
|
generator;
|
|
@@ -4506,6 +4706,7 @@ var init_Pipeline = __esm({
|
|
|
4506
4706
|
this._ephemeralHints = null;
|
|
4507
4707
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
4508
4708
|
}
|
|
4709
|
+
cards = diversityRerank(cards);
|
|
4509
4710
|
cards.sort((a, b) => b.score - a.score);
|
|
4510
4711
|
const tFilter = performance.now();
|
|
4511
4712
|
const result = cards.slice(0, limit);
|
|
@@ -4809,6 +5010,68 @@ var init_Pipeline = __esm({
|
|
|
4809
5010
|
// ---------------------------------------------------------------------------
|
|
4810
5011
|
// Card-space diagnostic
|
|
4811
5012
|
// ---------------------------------------------------------------------------
|
|
5013
|
+
/**
|
|
5014
|
+
* Commit-free forecast: score the user's full card space through the filter
|
|
5015
|
+
* chain and return the cards that are currently *reachable* (score >=
|
|
5016
|
+
* threshold), optionally nudged by caller-supplied hints and/or restricted
|
|
5017
|
+
* to cards the user hasn't seen yet.
|
|
5018
|
+
*
|
|
5019
|
+
* This is a GENERIC primitive — it returns scored, tag-hydrated cards and
|
|
5020
|
+
* stops there. It has no knowledge of any particular tag convention; callers
|
|
5021
|
+
* decide what the surviving cards mean (e.g. filter to their own "intro"
|
|
5022
|
+
* tag family). Nothing is written and no session is started.
|
|
5023
|
+
*
|
|
5024
|
+
* The optional `hints` are the "out-of-band kick": they run through the same
|
|
5025
|
+
* {@link applyHints} path a live replan uses, so the two semantics carry over —
|
|
5026
|
+
* - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
|
|
5027
|
+
* stays out), and
|
|
5028
|
+
* - `requireTags`/`requireCards` inject from the full pre-filter pool,
|
|
5029
|
+
* *bypassing* gating (use when you want a card regardless of reachability).
|
|
5030
|
+
* Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
|
|
5031
|
+
* user has already seen — pass `unseenOnly: false` if that matters.
|
|
5032
|
+
*
|
|
5033
|
+
* Cost note: like {@link diagnoseCardSpace}, this scans every card through the
|
|
5034
|
+
* filters, so it's heavier than a normal replan. Intended for one-shot
|
|
5035
|
+
* out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
|
|
5036
|
+
*
|
|
5037
|
+
* @param opts.hints Optional ephemeral hints to apply after the filter chain.
|
|
5038
|
+
* @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
|
|
5039
|
+
* @param opts.threshold Min score to count as reachable (default 0.10).
|
|
5040
|
+
* @param opts.limit Optional cap on results (already sorted desc).
|
|
5041
|
+
*/
|
|
5042
|
+
async forecast(opts) {
|
|
5043
|
+
const threshold = opts?.threshold ?? 0.1;
|
|
5044
|
+
const unseenOnly = opts?.unseenOnly ?? true;
|
|
5045
|
+
const courseId = this.course.getCourseID();
|
|
5046
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
5047
|
+
let cards = allCardIds.map((cardId) => ({
|
|
5048
|
+
cardId,
|
|
5049
|
+
courseId,
|
|
5050
|
+
score: 1,
|
|
5051
|
+
provenance: []
|
|
5052
|
+
}));
|
|
5053
|
+
cards = await this.hydrateTags(cards);
|
|
5054
|
+
const fullPool = cards.slice();
|
|
5055
|
+
const context = await this.buildContext();
|
|
5056
|
+
for (const filter of this.filters) {
|
|
5057
|
+
cards = await filter.transform(cards, context);
|
|
5058
|
+
}
|
|
5059
|
+
if (opts?.hints) {
|
|
5060
|
+
cards = this.applyHints(cards, opts.hints, fullPool);
|
|
5061
|
+
}
|
|
5062
|
+
cards = cards.filter((c) => c.score >= threshold);
|
|
5063
|
+
if (unseenOnly) {
|
|
5064
|
+
let encountered;
|
|
5065
|
+
try {
|
|
5066
|
+
encountered = new Set(await this.user.getSeenCards(courseId));
|
|
5067
|
+
} catch {
|
|
5068
|
+
encountered = /* @__PURE__ */ new Set();
|
|
5069
|
+
}
|
|
5070
|
+
cards = cards.filter((c) => !encountered.has(c.cardId));
|
|
5071
|
+
}
|
|
5072
|
+
cards.sort((a, b) => b.score - a.score);
|
|
5073
|
+
return opts?.limit ? cards.slice(0, opts.limit) : cards;
|
|
5074
|
+
}
|
|
4812
5075
|
/**
|
|
4813
5076
|
* Scan every card in the course through the filter chain and report
|
|
4814
5077
|
* how many are "well indicated" (score >= threshold) for the current user.
|
|
@@ -5074,6 +5337,7 @@ var init_3 = __esm({
|
|
|
5074
5337
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
5075
5338
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
5076
5339
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5340
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
5077
5341
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
5078
5342
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
5079
5343
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -5099,9 +5363,13 @@ var init_3 = __esm({
|
|
|
5099
5363
|
var navigators_exports = {};
|
|
5100
5364
|
__export(navigators_exports, {
|
|
5101
5365
|
ContentNavigator: () => ContentNavigator,
|
|
5366
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
5367
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
5102
5368
|
NavigatorRole: () => NavigatorRole,
|
|
5103
5369
|
NavigatorRoles: () => NavigatorRoles,
|
|
5104
5370
|
Navigators: () => Navigators,
|
|
5371
|
+
diversityRerank: () => diversityRerank,
|
|
5372
|
+
getActivePipeline: () => getActivePipeline,
|
|
5105
5373
|
getCardOrigin: () => getCardOrigin,
|
|
5106
5374
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
5107
5375
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -5185,6 +5453,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
5185
5453
|
var init_navigators = __esm({
|
|
5186
5454
|
"src/core/navigators/index.ts"() {
|
|
5187
5455
|
"use strict";
|
|
5456
|
+
init_diversityRerank();
|
|
5188
5457
|
init_PipelineDebugger();
|
|
5189
5458
|
init_logger();
|
|
5190
5459
|
init_();
|
|
@@ -10187,6 +10456,8 @@ __export(index_exports, {
|
|
|
10187
10456
|
ContentNavigator: () => ContentNavigator,
|
|
10188
10457
|
CouchDBToStaticPacker: () => CouchDBToStaticPacker,
|
|
10189
10458
|
CourseLookup: () => CourseLookup,
|
|
10459
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
10460
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
10190
10461
|
DocType: () => DocType,
|
|
10191
10462
|
DocTypePrefixes: () => DocTypePrefixes,
|
|
10192
10463
|
ENV: () => ENV,
|
|
@@ -10212,9 +10483,11 @@ __export(index_exports, {
|
|
|
10212
10483
|
computeSpread: () => computeSpread,
|
|
10213
10484
|
computeStrategyGradient: () => computeStrategyGradient,
|
|
10214
10485
|
createOrchestrationContext: () => createOrchestrationContext,
|
|
10486
|
+
diversityRerank: () => diversityRerank,
|
|
10215
10487
|
docIsDeleted: () => docIsDeleted,
|
|
10216
10488
|
endSessionTracking: () => endSessionTracking,
|
|
10217
10489
|
ensureAppDataDirectory: () => ensureAppDataDirectory,
|
|
10490
|
+
getActivePipeline: () => getActivePipeline,
|
|
10218
10491
|
getAppDataDirectory: () => getAppDataDirectory,
|
|
10219
10492
|
getCardHistoryID: () => getCardHistoryID,
|
|
10220
10493
|
getCardOrigin: () => getCardOrigin,
|
|
@@ -11323,8 +11596,17 @@ var ItemQueue = class {
|
|
|
11323
11596
|
* Merge new items into the front of the queue, skipping duplicates.
|
|
11324
11597
|
* Used by additive replans to inject high-quality candidates without
|
|
11325
11598
|
* discarding the existing queue contents.
|
|
11599
|
+
*
|
|
11600
|
+
* `forceFrontIds` carries the mandatory (`+INF`) cards in this batch — a
|
|
11601
|
+
* durable `requireCard`/`requireTag` re-asserted by every replan. An ordinary
|
|
11602
|
+
* duplicate is left in place (skip), but a mandatory one that's *already*
|
|
11603
|
+
* queued is pulled out of its current slot so it rejoins at the front in batch
|
|
11604
|
+
* order. Without this, an additive merge unshifts fresh non-required cards
|
|
11605
|
+
* ahead of an already-present required card, steadily burying it until it never
|
|
11606
|
+
* gets drawn — defeating the "must appear" guarantee. Returns the count of
|
|
11607
|
+
* genuinely new cards added (re-fronted duplicates are not counted).
|
|
11326
11608
|
*/
|
|
11327
|
-
mergeToFront(items, cardIdExtractor) {
|
|
11609
|
+
mergeToFront(items, cardIdExtractor, forceFrontIds) {
|
|
11328
11610
|
let added = 0;
|
|
11329
11611
|
const toInsert = [];
|
|
11330
11612
|
for (const item of items) {
|
|
@@ -11333,6 +11615,11 @@ var ItemQueue = class {
|
|
|
11333
11615
|
this.seenCardIds.push(cardId);
|
|
11334
11616
|
toInsert.push(item);
|
|
11335
11617
|
added++;
|
|
11618
|
+
} else if (forceFrontIds?.has(cardId)) {
|
|
11619
|
+
const idx = this.q.findIndex((qi) => cardIdExtractor(qi) === cardId);
|
|
11620
|
+
if (idx >= 0) {
|
|
11621
|
+
toInsert.push(...this.q.splice(idx, 1));
|
|
11622
|
+
}
|
|
11336
11623
|
}
|
|
11337
11624
|
}
|
|
11338
11625
|
this.q.unshift(...toInsert);
|
|
@@ -14450,7 +14737,16 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14450
14737
|
mixedWeighted
|
|
14451
14738
|
);
|
|
14452
14739
|
const reviewWeighted = mixedWeighted.filter((w) => getCardOrigin(w) === "review").slice(0, this._initialReviewCap);
|
|
14453
|
-
const
|
|
14740
|
+
const newCandidates = mixedWeighted.filter(
|
|
14741
|
+
(w) => getCardOrigin(w) === "new" && !this._servedCardIds.has(w.cardId)
|
|
14742
|
+
);
|
|
14743
|
+
const mandatoryWeighted = newCandidates.filter((w) => w.score === Number.POSITIVE_INFINITY);
|
|
14744
|
+
const optionalWeighted = newCandidates.filter((w) => w.score !== Number.POSITIVE_INFINITY);
|
|
14745
|
+
const newWeighted = [
|
|
14746
|
+
...mandatoryWeighted,
|
|
14747
|
+
...optionalWeighted.slice(0, Math.max(0, newLimit - mandatoryWeighted.length))
|
|
14748
|
+
];
|
|
14749
|
+
const mandatoryIds = new Set(mandatoryWeighted.map((w) => w.cardId));
|
|
14454
14750
|
logger.debug(`[reviews] got ${reviewWeighted.length} reviews from mixer`);
|
|
14455
14751
|
let report = replan ? "Replan content:\n" : "Mixed content session created with:\n";
|
|
14456
14752
|
if (!replan) {
|
|
@@ -14485,7 +14781,7 @@ var SessionController = class _SessionController extends Loggable {
|
|
|
14485
14781
|
`;
|
|
14486
14782
|
}
|
|
14487
14783
|
if (additive) {
|
|
14488
|
-
const added = this.newQ.mergeToFront(newItems, (item) => item.cardID);
|
|
14784
|
+
const added = this.newQ.mergeToFront(newItems, (item) => item.cardID, mandatoryIds);
|
|
14489
14785
|
report += `Additive merge: ${added} new cards added to front of newQ
|
|
14490
14786
|
`;
|
|
14491
14787
|
} else if (replan) {
|
|
@@ -14817,6 +15113,8 @@ init_factory();
|
|
|
14817
15113
|
ContentNavigator,
|
|
14818
15114
|
CouchDBToStaticPacker,
|
|
14819
15115
|
CourseLookup,
|
|
15116
|
+
DIVERSITY_FLOOR,
|
|
15117
|
+
DIVERSITY_STRENGTH,
|
|
14820
15118
|
DocType,
|
|
14821
15119
|
DocTypePrefixes,
|
|
14822
15120
|
ENV,
|
|
@@ -14842,9 +15140,11 @@ init_factory();
|
|
|
14842
15140
|
computeSpread,
|
|
14843
15141
|
computeStrategyGradient,
|
|
14844
15142
|
createOrchestrationContext,
|
|
15143
|
+
diversityRerank,
|
|
14845
15144
|
docIsDeleted,
|
|
14846
15145
|
endSessionTracking,
|
|
14847
15146
|
ensureAppDataDirectory,
|
|
15147
|
+
getActivePipeline,
|
|
14848
15148
|
getAppDataDirectory,
|
|
14849
15149
|
getCardHistoryID,
|
|
14850
15150
|
getCardOrigin,
|