@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/core/index.d.cts
CHANGED
|
@@ -304,6 +304,42 @@ interface CardFilter {
|
|
|
304
304
|
*/
|
|
305
305
|
type CardFilterFactory<TConfig = unknown> = (config: TConfig) => CardFilter;
|
|
306
306
|
|
|
307
|
+
interface DiversityRerankOptions {
|
|
308
|
+
/**
|
|
309
|
+
* How hard repetition is penalised. Larger → steeper demotion of repeated
|
|
310
|
+
* distinctive tags. Penalty = 1 / (1 + strength·load).
|
|
311
|
+
*/
|
|
312
|
+
strength?: number;
|
|
313
|
+
/**
|
|
314
|
+
* Minimum penalty multiplier. A card is never demoted below `floor × score`,
|
|
315
|
+
* however much it repeats. Keeps a strong-but-repeated card from being driven
|
|
316
|
+
* under downstream "well-indicated" thresholds (which would mislabel it as
|
|
317
|
+
* filler and could trigger spurious quality-replans). Tunes "perturb ordering"
|
|
318
|
+
* vs "annihilate candidates."
|
|
319
|
+
*/
|
|
320
|
+
floor?: number;
|
|
321
|
+
}
|
|
322
|
+
/** Default repetition strength. See DiversityRerankOptions.strength. */
|
|
323
|
+
declare const DIVERSITY_STRENGTH = 0.6;
|
|
324
|
+
/** Default penalty floor. See DiversityRerankOptions.floor. */
|
|
325
|
+
declare const DIVERSITY_FLOOR = 0.3;
|
|
326
|
+
/**
|
|
327
|
+
* Re-rank a scored candidate pool for answer/concept variety.
|
|
328
|
+
*
|
|
329
|
+
* Pure: returns a new array (diversified order, adjusted scores, appended
|
|
330
|
+
* provenance) and does not mutate the input cards. Cards entering are assumed
|
|
331
|
+
* to have score > 0 (the Pipeline strips zero-score cards before this stage).
|
|
332
|
+
* Non-finite scores (mandatory `requireCards`, score = +Infinity) are emitted
|
|
333
|
+
* untouched and still count toward repetition for later cards.
|
|
334
|
+
*
|
|
335
|
+
* @param cards - Post-filter, post-hint candidates.
|
|
336
|
+
* @param opts - Optional strength/floor overrides (defaults are sane and
|
|
337
|
+
* course-general; promote to strategy config if you ever want
|
|
338
|
+
* this learnable under the orchestration layer).
|
|
339
|
+
* @returns Cards in diversified order with penalised scores.
|
|
340
|
+
*/
|
|
341
|
+
declare function diversityRerank(cards: WeightedCard[], opts?: DiversityRerankOptions): WeightedCard[];
|
|
342
|
+
|
|
307
343
|
/**
|
|
308
344
|
* Diagnosis of the full card space for the current user.
|
|
309
345
|
*/
|
|
@@ -706,4 +742,4 @@ declare const userDBDebugAPI: {
|
|
|
706
742
|
*/
|
|
707
743
|
declare function mountUserDBDebugger(): void;
|
|
708
744
|
|
|
709
|
-
export { type BulkCardProcessorConfig, type CardFilter, type CardFilterFactory, CardHistory, CardRecord, CourseDBInterface, DocType, DocTypePrefixes, type FilterContext, type FilterImpact, type GeneratorSummary, type GradientObservation, type GradientResult, type ImportResult, LearnableWeight, Loggable, OrchestrationContext, type PeriodUpdateInput, type PeriodUpdateResult, type PipelineRunReport, QuestionRecord, ReplanHints, type SignalConfig, StrategyContribution, type StrategyLearningState, type StrategyStateDoc, type StrategyStateId, UserDBInterface, UserOutcomeRecord, WeightedCard, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig };
|
|
745
|
+
export { type BulkCardProcessorConfig, type CardFilter, type CardFilterFactory, CardHistory, CardRecord, CourseDBInterface, DIVERSITY_FLOOR, DIVERSITY_STRENGTH, type DiversityRerankOptions, DocType, DocTypePrefixes, type FilterContext, type FilterImpact, type GeneratorSummary, type GradientObservation, type GradientResult, type ImportResult, LearnableWeight, Loggable, OrchestrationContext, type PeriodUpdateInput, type PeriodUpdateResult, type PipelineRunReport, QuestionRecord, ReplanHints, type SignalConfig, StrategyContribution, type StrategyLearningState, type StrategyStateDoc, type StrategyStateId, UserDBInterface, UserOutcomeRecord, WeightedCard, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, diversityRerank, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig };
|
package/dist/core/index.d.ts
CHANGED
|
@@ -304,6 +304,42 @@ interface CardFilter {
|
|
|
304
304
|
*/
|
|
305
305
|
type CardFilterFactory<TConfig = unknown> = (config: TConfig) => CardFilter;
|
|
306
306
|
|
|
307
|
+
interface DiversityRerankOptions {
|
|
308
|
+
/**
|
|
309
|
+
* How hard repetition is penalised. Larger → steeper demotion of repeated
|
|
310
|
+
* distinctive tags. Penalty = 1 / (1 + strength·load).
|
|
311
|
+
*/
|
|
312
|
+
strength?: number;
|
|
313
|
+
/**
|
|
314
|
+
* Minimum penalty multiplier. A card is never demoted below `floor × score`,
|
|
315
|
+
* however much it repeats. Keeps a strong-but-repeated card from being driven
|
|
316
|
+
* under downstream "well-indicated" thresholds (which would mislabel it as
|
|
317
|
+
* filler and could trigger spurious quality-replans). Tunes "perturb ordering"
|
|
318
|
+
* vs "annihilate candidates."
|
|
319
|
+
*/
|
|
320
|
+
floor?: number;
|
|
321
|
+
}
|
|
322
|
+
/** Default repetition strength. See DiversityRerankOptions.strength. */
|
|
323
|
+
declare const DIVERSITY_STRENGTH = 0.6;
|
|
324
|
+
/** Default penalty floor. See DiversityRerankOptions.floor. */
|
|
325
|
+
declare const DIVERSITY_FLOOR = 0.3;
|
|
326
|
+
/**
|
|
327
|
+
* Re-rank a scored candidate pool for answer/concept variety.
|
|
328
|
+
*
|
|
329
|
+
* Pure: returns a new array (diversified order, adjusted scores, appended
|
|
330
|
+
* provenance) and does not mutate the input cards. Cards entering are assumed
|
|
331
|
+
* to have score > 0 (the Pipeline strips zero-score cards before this stage).
|
|
332
|
+
* Non-finite scores (mandatory `requireCards`, score = +Infinity) are emitted
|
|
333
|
+
* untouched and still count toward repetition for later cards.
|
|
334
|
+
*
|
|
335
|
+
* @param cards - Post-filter, post-hint candidates.
|
|
336
|
+
* @param opts - Optional strength/floor overrides (defaults are sane and
|
|
337
|
+
* course-general; promote to strategy config if you ever want
|
|
338
|
+
* this learnable under the orchestration layer).
|
|
339
|
+
* @returns Cards in diversified order with penalised scores.
|
|
340
|
+
*/
|
|
341
|
+
declare function diversityRerank(cards: WeightedCard[], opts?: DiversityRerankOptions): WeightedCard[];
|
|
342
|
+
|
|
307
343
|
/**
|
|
308
344
|
* Diagnosis of the full card space for the current user.
|
|
309
345
|
*/
|
|
@@ -706,4 +742,4 @@ declare const userDBDebugAPI: {
|
|
|
706
742
|
*/
|
|
707
743
|
declare function mountUserDBDebugger(): void;
|
|
708
744
|
|
|
709
|
-
export { type BulkCardProcessorConfig, type CardFilter, type CardFilterFactory, CardHistory, CardRecord, CourseDBInterface, DocType, DocTypePrefixes, type FilterContext, type FilterImpact, type GeneratorSummary, type GradientObservation, type GradientResult, type ImportResult, LearnableWeight, Loggable, OrchestrationContext, type PeriodUpdateInput, type PeriodUpdateResult, type PipelineRunReport, QuestionRecord, ReplanHints, type SignalConfig, StrategyContribution, type StrategyLearningState, type StrategyStateDoc, type StrategyStateId, UserDBInterface, UserOutcomeRecord, WeightedCard, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig };
|
|
745
|
+
export { type BulkCardProcessorConfig, type CardFilter, type CardFilterFactory, CardHistory, CardRecord, CourseDBInterface, DIVERSITY_FLOOR, DIVERSITY_STRENGTH, type DiversityRerankOptions, DocType, DocTypePrefixes, type FilterContext, type FilterImpact, type GeneratorSummary, type GradientObservation, type GradientResult, type ImportResult, LearnableWeight, Loggable, OrchestrationContext, type PeriodUpdateInput, type PeriodUpdateResult, type PipelineRunReport, QuestionRecord, ReplanHints, type SignalConfig, StrategyContribution, type StrategyLearningState, type StrategyStateDoc, type StrategyStateId, UserDBInterface, UserOutcomeRecord, WeightedCard, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, diversityRerank, docIsDeleted, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig };
|
package/dist/core/index.js
CHANGED
|
@@ -719,6 +719,95 @@ var init_courseLookupDB = __esm({
|
|
|
719
719
|
}
|
|
720
720
|
});
|
|
721
721
|
|
|
722
|
+
// src/core/navigators/diversityRerank.ts
|
|
723
|
+
var diversityRerank_exports = {};
|
|
724
|
+
__export(diversityRerank_exports, {
|
|
725
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
726
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
727
|
+
diversityRerank: () => diversityRerank
|
|
728
|
+
});
|
|
729
|
+
function diversityRerank(cards, opts = {}) {
|
|
730
|
+
const strength = opts.strength ?? DIVERSITY_STRENGTH;
|
|
731
|
+
const floor = opts.floor ?? DIVERSITY_FLOOR;
|
|
732
|
+
const n = cards.length;
|
|
733
|
+
if (n <= 1) return cards;
|
|
734
|
+
const df = /* @__PURE__ */ new Map();
|
|
735
|
+
for (const card of cards) {
|
|
736
|
+
for (const tag of card.tags ?? []) {
|
|
737
|
+
df.set(tag, (df.get(tag) ?? 0) + 1);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const idf = /* @__PURE__ */ new Map();
|
|
741
|
+
for (const [tag, freq] of df) {
|
|
742
|
+
idf.set(tag, Math.log(n / freq));
|
|
743
|
+
}
|
|
744
|
+
const remaining = [...cards];
|
|
745
|
+
const emittedCount = /* @__PURE__ */ new Map();
|
|
746
|
+
const out = [];
|
|
747
|
+
const repetitionLoad = (card) => {
|
|
748
|
+
let load = 0;
|
|
749
|
+
for (const tag of card.tags ?? []) {
|
|
750
|
+
const seen = emittedCount.get(tag);
|
|
751
|
+
if (seen) load += (idf.get(tag) ?? 0) * seen;
|
|
752
|
+
}
|
|
753
|
+
return load;
|
|
754
|
+
};
|
|
755
|
+
while (remaining.length > 0) {
|
|
756
|
+
let bestIdx = 0;
|
|
757
|
+
let bestValue = -Infinity;
|
|
758
|
+
let bestPenalty = 1;
|
|
759
|
+
let bestLoad = 0;
|
|
760
|
+
for (let i = 0; i < remaining.length; i++) {
|
|
761
|
+
const card = remaining[i];
|
|
762
|
+
const load = repetitionLoad(card);
|
|
763
|
+
const penalty = load > 0 ? Math.max(floor, 1 / (1 + strength * load)) : 1;
|
|
764
|
+
const value = card.score * penalty;
|
|
765
|
+
if (value > bestValue) {
|
|
766
|
+
bestValue = value;
|
|
767
|
+
bestIdx = i;
|
|
768
|
+
bestPenalty = penalty;
|
|
769
|
+
bestLoad = load;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
const [picked] = remaining.splice(bestIdx, 1);
|
|
773
|
+
if (Number.isFinite(picked.score) && bestPenalty < 1) {
|
|
774
|
+
const newScore = picked.score * bestPenalty;
|
|
775
|
+
out.push({
|
|
776
|
+
...picked,
|
|
777
|
+
score: newScore,
|
|
778
|
+
provenance: [
|
|
779
|
+
...picked.provenance,
|
|
780
|
+
{
|
|
781
|
+
strategy: STRATEGY,
|
|
782
|
+
strategyId: STRATEGY_ID,
|
|
783
|
+
strategyName: STRATEGY_NAME,
|
|
784
|
+
action: "penalized",
|
|
785
|
+
score: newScore,
|
|
786
|
+
reason: `repeated tags (load ${bestLoad.toFixed(2)}) \u2192 \xD7${bestPenalty.toFixed(2)}`
|
|
787
|
+
}
|
|
788
|
+
]
|
|
789
|
+
});
|
|
790
|
+
} else {
|
|
791
|
+
out.push(picked);
|
|
792
|
+
}
|
|
793
|
+
for (const tag of picked.tags ?? []) {
|
|
794
|
+
emittedCount.set(tag, (emittedCount.get(tag) ?? 0) + 1);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return out;
|
|
798
|
+
}
|
|
799
|
+
var DIVERSITY_STRENGTH, DIVERSITY_FLOOR, STRATEGY, STRATEGY_ID, STRATEGY_NAME;
|
|
800
|
+
var init_diversityRerank = __esm({
|
|
801
|
+
"src/core/navigators/diversityRerank.ts"() {
|
|
802
|
+
"use strict";
|
|
803
|
+
DIVERSITY_STRENGTH = 0.6;
|
|
804
|
+
DIVERSITY_FLOOR = 0.3;
|
|
805
|
+
STRATEGY = "diversityRerank";
|
|
806
|
+
STRATEGY_ID = "DIVERSITY_RERANK";
|
|
807
|
+
STRATEGY_NAME = "Diversity Re-rank";
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
|
|
722
811
|
// src/core/navigators/PipelineDebugger.ts
|
|
723
812
|
var PipelineDebugger_exports = {};
|
|
724
813
|
__export(PipelineDebugger_exports, {
|
|
@@ -1914,7 +2003,7 @@ function shuffleInPlace(arr) {
|
|
|
1914
2003
|
function pickTopByScore(cards, limit) {
|
|
1915
2004
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1916
2005
|
}
|
|
1917
|
-
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;
|
|
2006
|
+
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;
|
|
1918
2007
|
var init_prescribed = __esm({
|
|
1919
2008
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1920
2009
|
"use strict";
|
|
@@ -1925,9 +2014,12 @@ var init_prescribed = __esm({
|
|
|
1925
2014
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1926
2015
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1927
2016
|
DEFAULT_MIN_COUNT = 3;
|
|
2017
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
2018
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
1928
2019
|
BASE_TARGET_SCORE = 1;
|
|
1929
2020
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1930
2021
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
2022
|
+
BASE_PRACTICE_SCORE = 1;
|
|
1931
2023
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1932
2024
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1933
2025
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -2035,7 +2127,18 @@ var init_prescribed = __esm({
|
|
|
2035
2127
|
courseId,
|
|
2036
2128
|
emittedIds
|
|
2037
2129
|
);
|
|
2038
|
-
|
|
2130
|
+
const practiceCards = this.buildPracticeCards({
|
|
2131
|
+
group,
|
|
2132
|
+
courseId,
|
|
2133
|
+
emittedIds,
|
|
2134
|
+
cardsByTag,
|
|
2135
|
+
hierarchyConfigs,
|
|
2136
|
+
userTagElo,
|
|
2137
|
+
userGlobalElo,
|
|
2138
|
+
activeIds,
|
|
2139
|
+
seenIds
|
|
2140
|
+
});
|
|
2141
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
2039
2142
|
}
|
|
2040
2143
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2041
2144
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -2063,6 +2166,7 @@ var init_prescribed = __esm({
|
|
|
2063
2166
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
2064
2167
|
for (const card of finalCards) {
|
|
2065
2168
|
const prov = card.provenance[0];
|
|
2169
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
2066
2170
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
2067
2171
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
2068
2172
|
if (!groupId) continue;
|
|
@@ -2132,7 +2236,12 @@ var init_prescribed = __esm({
|
|
|
2132
2236
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
2133
2237
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
2134
2238
|
},
|
|
2135
|
-
retireOnEncounter: raw.retireOnEncounter !== false
|
|
2239
|
+
retireOnEncounter: raw.retireOnEncounter !== false,
|
|
2240
|
+
practiceTagPatterns: dedupe(
|
|
2241
|
+
Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
|
|
2242
|
+
),
|
|
2243
|
+
practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
|
|
2244
|
+
maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
|
|
2136
2245
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
2137
2246
|
return { groups };
|
|
2138
2247
|
} catch {
|
|
@@ -2355,6 +2464,92 @@ var init_prescribed = __esm({
|
|
|
2355
2464
|
}
|
|
2356
2465
|
return cards;
|
|
2357
2466
|
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Emit drill cards for *unlocked-but-under-practiced* skills.
|
|
2469
|
+
*
|
|
2470
|
+
* For each course tag matching the group's `practiceTagPatterns` that is both
|
|
2471
|
+
* unlocked (all hierarchy prerequisites met — i.e. the learner has been
|
|
2472
|
+
* introduced to it) and under-practiced (per-tag attempt count below
|
|
2473
|
+
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2474
|
+
* into the candidate pool. It exists because global-ELO retrieval
|
|
2475
|
+
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2476
|
+
* freshly-introduced skill — putting them in the pool here lets the pipeline's
|
|
2477
|
+
* scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
|
|
2478
|
+
* this method's job; it only guarantees presence.
|
|
2479
|
+
*
|
|
2480
|
+
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2481
|
+
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
2482
|
+
*/
|
|
2483
|
+
buildPracticeCards(args) {
|
|
2484
|
+
const {
|
|
2485
|
+
group,
|
|
2486
|
+
courseId,
|
|
2487
|
+
emittedIds,
|
|
2488
|
+
cardsByTag,
|
|
2489
|
+
hierarchyConfigs,
|
|
2490
|
+
userTagElo,
|
|
2491
|
+
userGlobalElo,
|
|
2492
|
+
activeIds,
|
|
2493
|
+
seenIds
|
|
2494
|
+
} = args;
|
|
2495
|
+
const patterns = group.practiceTagPatterns ?? [];
|
|
2496
|
+
if (patterns.length === 0) return [];
|
|
2497
|
+
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2498
|
+
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2499
|
+
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2500
|
+
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2501
|
+
);
|
|
2502
|
+
if (practiceTags.length === 0) return [];
|
|
2503
|
+
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2504
|
+
supportTags: practiceTags,
|
|
2505
|
+
cardsByTag,
|
|
2506
|
+
activeIds,
|
|
2507
|
+
seenIds,
|
|
2508
|
+
excludedIds: emittedIds,
|
|
2509
|
+
limit: maxPractice
|
|
2510
|
+
});
|
|
2511
|
+
if (practiceCardIds.length === 0) return [];
|
|
2512
|
+
logger.info(
|
|
2513
|
+
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2514
|
+
);
|
|
2515
|
+
const cards = [];
|
|
2516
|
+
for (const cardId of practiceCardIds) {
|
|
2517
|
+
emittedIds.add(cardId);
|
|
2518
|
+
cards.push({
|
|
2519
|
+
cardId,
|
|
2520
|
+
courseId,
|
|
2521
|
+
score: BASE_PRACTICE_SCORE,
|
|
2522
|
+
provenance: [
|
|
2523
|
+
{
|
|
2524
|
+
strategy: "prescribed",
|
|
2525
|
+
strategyName: this.strategyName || this.name,
|
|
2526
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2527
|
+
action: "generated",
|
|
2528
|
+
score: BASE_PRACTICE_SCORE,
|
|
2529
|
+
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2530
|
+
}
|
|
2531
|
+
]
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
return cards;
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* True for a skill that was *gated and is now reached*: it has at least one
|
|
2538
|
+
* declared hierarchy prerequisite set, and every set is fully satisfied by the
|
|
2539
|
+
* learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
|
|
2540
|
+
* — an ungated tag was never "introduced" in the curricular sense, so it isn't
|
|
2541
|
+
* a post-intro drill target (e.g. whole-word spelling tags that share the
|
|
2542
|
+
* `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
|
|
2543
|
+
* ELO retrieval. This is the precise population the retrieval gap strands:
|
|
2544
|
+
* just-unlocked, low-ELO skills.
|
|
2545
|
+
*/
|
|
2546
|
+
isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
|
|
2547
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
2548
|
+
if (prereqSets.length === 0) return false;
|
|
2549
|
+
return prereqSets.every(
|
|
2550
|
+
(prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
2551
|
+
);
|
|
2552
|
+
}
|
|
2358
2553
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2359
2554
|
if (supportTags.length === 0) {
|
|
2360
2555
|
return [];
|
|
@@ -4148,7 +4343,7 @@ function logResultCards(cards) {
|
|
|
4148
4343
|
for (let i = 0; i < cards.length; i++) {
|
|
4149
4344
|
const c = cards[i];
|
|
4150
4345
|
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
4151
|
-
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
4346
|
+
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) => {
|
|
4152
4347
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
4153
4348
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
4154
4349
|
}).join(" | ");
|
|
@@ -4180,6 +4375,7 @@ var init_Pipeline = __esm({
|
|
|
4180
4375
|
init_logger();
|
|
4181
4376
|
init_orchestration();
|
|
4182
4377
|
init_PipelineDebugger();
|
|
4378
|
+
init_diversityRerank();
|
|
4183
4379
|
VERBOSE_RESULTS = true;
|
|
4184
4380
|
Pipeline = class extends ContentNavigator {
|
|
4185
4381
|
generator;
|
|
@@ -4353,6 +4549,7 @@ var init_Pipeline = __esm({
|
|
|
4353
4549
|
this._ephemeralHints = null;
|
|
4354
4550
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
4355
4551
|
}
|
|
4552
|
+
cards = diversityRerank(cards);
|
|
4356
4553
|
cards.sort((a, b) => b.score - a.score);
|
|
4357
4554
|
const tFilter = performance.now();
|
|
4358
4555
|
const result = cards.slice(0, limit);
|
|
@@ -4921,6 +5118,7 @@ var init_3 = __esm({
|
|
|
4921
5118
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4922
5119
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4923
5120
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5121
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4924
5122
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
4925
5123
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
4926
5124
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -4946,9 +5144,12 @@ var init_3 = __esm({
|
|
|
4946
5144
|
var navigators_exports = {};
|
|
4947
5145
|
__export(navigators_exports, {
|
|
4948
5146
|
ContentNavigator: () => ContentNavigator,
|
|
5147
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
5148
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
4949
5149
|
NavigatorRole: () => NavigatorRole,
|
|
4950
5150
|
NavigatorRoles: () => NavigatorRoles,
|
|
4951
5151
|
Navigators: () => Navigators,
|
|
5152
|
+
diversityRerank: () => diversityRerank,
|
|
4952
5153
|
getCardOrigin: () => getCardOrigin,
|
|
4953
5154
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
4954
5155
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -5032,6 +5233,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
5032
5233
|
var init_navigators = __esm({
|
|
5033
5234
|
"src/core/navigators/index.ts"() {
|
|
5034
5235
|
"use strict";
|
|
5236
|
+
init_diversityRerank();
|
|
5035
5237
|
init_PipelineDebugger();
|
|
5036
5238
|
init_logger();
|
|
5037
5239
|
init_();
|
|
@@ -8039,6 +8241,8 @@ Examples:
|
|
|
8039
8241
|
var core_exports = {};
|
|
8040
8242
|
__export(core_exports, {
|
|
8041
8243
|
ContentNavigator: () => ContentNavigator,
|
|
8244
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
8245
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
8042
8246
|
DocType: () => DocType,
|
|
8043
8247
|
DocTypePrefixes: () => DocTypePrefixes,
|
|
8044
8248
|
GuestUsername: () => GuestUsername,
|
|
@@ -8055,6 +8259,7 @@ __export(core_exports, {
|
|
|
8055
8259
|
computeSpread: () => computeSpread,
|
|
8056
8260
|
computeStrategyGradient: () => computeStrategyGradient,
|
|
8057
8261
|
createOrchestrationContext: () => createOrchestrationContext,
|
|
8262
|
+
diversityRerank: () => diversityRerank,
|
|
8058
8263
|
docIsDeleted: () => docIsDeleted,
|
|
8059
8264
|
getCardHistoryID: () => getCardHistoryID,
|
|
8060
8265
|
getCardOrigin: () => getCardOrigin,
|
|
@@ -8104,6 +8309,8 @@ init_core();
|
|
|
8104
8309
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8105
8310
|
0 && (module.exports = {
|
|
8106
8311
|
ContentNavigator,
|
|
8312
|
+
DIVERSITY_FLOOR,
|
|
8313
|
+
DIVERSITY_STRENGTH,
|
|
8107
8314
|
DocType,
|
|
8108
8315
|
DocTypePrefixes,
|
|
8109
8316
|
GuestUsername,
|
|
@@ -8120,6 +8327,7 @@ init_core();
|
|
|
8120
8327
|
computeSpread,
|
|
8121
8328
|
computeStrategyGradient,
|
|
8122
8329
|
createOrchestrationContext,
|
|
8330
|
+
diversityRerank,
|
|
8123
8331
|
docIsDeleted,
|
|
8124
8332
|
getCardHistoryID,
|
|
8125
8333
|
getCardOrigin,
|