@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/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, {
|
|
@@ -1786,13 +1875,14 @@ var elo_exports = {};
|
|
|
1786
1875
|
__export(elo_exports, {
|
|
1787
1876
|
default: () => ELONavigator
|
|
1788
1877
|
});
|
|
1789
|
-
var import_common5, ELONavigator;
|
|
1878
|
+
var import_common5, ELO_RELEVANCE_SIGMA, ELONavigator;
|
|
1790
1879
|
var init_elo = __esm({
|
|
1791
1880
|
"src/core/navigators/generators/elo.ts"() {
|
|
1792
1881
|
"use strict";
|
|
1793
1882
|
init_navigators();
|
|
1794
1883
|
import_common5 = require("@vue-skuilder/common");
|
|
1795
1884
|
init_logger();
|
|
1885
|
+
ELO_RELEVANCE_SIGMA = 300;
|
|
1796
1886
|
ELONavigator = class extends ContentNavigator {
|
|
1797
1887
|
/** Human-readable name for CardGenerator interface */
|
|
1798
1888
|
name;
|
|
@@ -1832,8 +1922,8 @@ var init_elo = __esm({
|
|
|
1832
1922
|
const scored = newCards.map((c) => {
|
|
1833
1923
|
const cardElo = c.elo ?? 1e3;
|
|
1834
1924
|
const distance = Math.abs(cardElo - userGlobalElo);
|
|
1835
|
-
const
|
|
1836
|
-
const samplingKey =
|
|
1925
|
+
const relevance = Math.exp(-((distance / ELO_RELEVANCE_SIGMA) ** 2));
|
|
1926
|
+
const samplingKey = relevance * (0.5 + 0.5 * Math.random());
|
|
1837
1927
|
return {
|
|
1838
1928
|
cardId: c.cardID,
|
|
1839
1929
|
courseId: c.courseID,
|
|
@@ -1845,7 +1935,7 @@ var init_elo = __esm({
|
|
|
1845
1935
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-ELO-default",
|
|
1846
1936
|
action: "generated",
|
|
1847
1937
|
score: samplingKey,
|
|
1848
|
-
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}),
|
|
1938
|
+
reason: `ELO distance ${Math.round(distance)} (card: ${Math.round(cardElo)}, user: ${Math.round(userGlobalElo)}), relevance ${relevance.toFixed(3)}, key ${samplingKey.toFixed(3)}`
|
|
1849
1939
|
}
|
|
1850
1940
|
]
|
|
1851
1941
|
};
|
|
@@ -1913,7 +2003,7 @@ function shuffleInPlace(arr) {
|
|
|
1913
2003
|
function pickTopByScore(cards, limit) {
|
|
1914
2004
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1915
2005
|
}
|
|
1916
|
-
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;
|
|
1917
2007
|
var init_prescribed = __esm({
|
|
1918
2008
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1919
2009
|
"use strict";
|
|
@@ -1924,9 +2014,12 @@ var init_prescribed = __esm({
|
|
|
1924
2014
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1925
2015
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1926
2016
|
DEFAULT_MIN_COUNT = 3;
|
|
2017
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
2018
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
1927
2019
|
BASE_TARGET_SCORE = 1;
|
|
1928
2020
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1929
2021
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
2022
|
+
BASE_PRACTICE_SCORE = 1;
|
|
1930
2023
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1931
2024
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1932
2025
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -2034,7 +2127,18 @@ var init_prescribed = __esm({
|
|
|
2034
2127
|
courseId,
|
|
2035
2128
|
emittedIds
|
|
2036
2129
|
);
|
|
2037
|
-
|
|
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);
|
|
2038
2142
|
}
|
|
2039
2143
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2040
2144
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -2062,6 +2166,7 @@ var init_prescribed = __esm({
|
|
|
2062
2166
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
2063
2167
|
for (const card of finalCards) {
|
|
2064
2168
|
const prov = card.provenance[0];
|
|
2169
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
2065
2170
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
2066
2171
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
2067
2172
|
if (!groupId) continue;
|
|
@@ -2131,7 +2236,12 @@ var init_prescribed = __esm({
|
|
|
2131
2236
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
2132
2237
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
2133
2238
|
},
|
|
2134
|
-
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
|
|
2135
2245
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
2136
2246
|
return { groups };
|
|
2137
2247
|
} catch {
|
|
@@ -2354,6 +2464,92 @@ var init_prescribed = __esm({
|
|
|
2354
2464
|
}
|
|
2355
2465
|
return cards;
|
|
2356
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
|
+
}
|
|
2357
2553
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2358
2554
|
if (supportTags.length === 0) {
|
|
2359
2555
|
return [];
|
|
@@ -4147,7 +4343,7 @@ function logResultCards(cards) {
|
|
|
4147
4343
|
for (let i = 0; i < cards.length; i++) {
|
|
4148
4344
|
const c = cards[i];
|
|
4149
4345
|
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
4150
|
-
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) => {
|
|
4151
4347
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
4152
4348
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
4153
4349
|
}).join(" | ");
|
|
@@ -4179,6 +4375,7 @@ var init_Pipeline = __esm({
|
|
|
4179
4375
|
init_logger();
|
|
4180
4376
|
init_orchestration();
|
|
4181
4377
|
init_PipelineDebugger();
|
|
4378
|
+
init_diversityRerank();
|
|
4182
4379
|
VERBOSE_RESULTS = true;
|
|
4183
4380
|
Pipeline = class extends ContentNavigator {
|
|
4184
4381
|
generator;
|
|
@@ -4352,6 +4549,7 @@ var init_Pipeline = __esm({
|
|
|
4352
4549
|
this._ephemeralHints = null;
|
|
4353
4550
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
4354
4551
|
}
|
|
4552
|
+
cards = diversityRerank(cards);
|
|
4355
4553
|
cards.sort((a, b) => b.score - a.score);
|
|
4356
4554
|
const tFilter = performance.now();
|
|
4357
4555
|
const result = cards.slice(0, limit);
|
|
@@ -4920,6 +5118,7 @@ var init_3 = __esm({
|
|
|
4920
5118
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4921
5119
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4922
5120
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5121
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4923
5122
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
4924
5123
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
4925
5124
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -4945,9 +5144,12 @@ var init_3 = __esm({
|
|
|
4945
5144
|
var navigators_exports = {};
|
|
4946
5145
|
__export(navigators_exports, {
|
|
4947
5146
|
ContentNavigator: () => ContentNavigator,
|
|
5147
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
5148
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
4948
5149
|
NavigatorRole: () => NavigatorRole,
|
|
4949
5150
|
NavigatorRoles: () => NavigatorRoles,
|
|
4950
5151
|
Navigators: () => Navigators,
|
|
5152
|
+
diversityRerank: () => diversityRerank,
|
|
4951
5153
|
getCardOrigin: () => getCardOrigin,
|
|
4952
5154
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
4953
5155
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -5031,6 +5233,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
5031
5233
|
var init_navigators = __esm({
|
|
5032
5234
|
"src/core/navigators/index.ts"() {
|
|
5033
5235
|
"use strict";
|
|
5236
|
+
init_diversityRerank();
|
|
5034
5237
|
init_PipelineDebugger();
|
|
5035
5238
|
init_logger();
|
|
5036
5239
|
init_();
|
|
@@ -8038,6 +8241,8 @@ Examples:
|
|
|
8038
8241
|
var core_exports = {};
|
|
8039
8242
|
__export(core_exports, {
|
|
8040
8243
|
ContentNavigator: () => ContentNavigator,
|
|
8244
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
8245
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
8041
8246
|
DocType: () => DocType,
|
|
8042
8247
|
DocTypePrefixes: () => DocTypePrefixes,
|
|
8043
8248
|
GuestUsername: () => GuestUsername,
|
|
@@ -8054,6 +8259,7 @@ __export(core_exports, {
|
|
|
8054
8259
|
computeSpread: () => computeSpread,
|
|
8055
8260
|
computeStrategyGradient: () => computeStrategyGradient,
|
|
8056
8261
|
createOrchestrationContext: () => createOrchestrationContext,
|
|
8262
|
+
diversityRerank: () => diversityRerank,
|
|
8057
8263
|
docIsDeleted: () => docIsDeleted,
|
|
8058
8264
|
getCardHistoryID: () => getCardHistoryID,
|
|
8059
8265
|
getCardOrigin: () => getCardOrigin,
|
|
@@ -8103,6 +8309,8 @@ init_core();
|
|
|
8103
8309
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8104
8310
|
0 && (module.exports = {
|
|
8105
8311
|
ContentNavigator,
|
|
8312
|
+
DIVERSITY_FLOOR,
|
|
8313
|
+
DIVERSITY_STRENGTH,
|
|
8106
8314
|
DocType,
|
|
8107
8315
|
DocTypePrefixes,
|
|
8108
8316
|
GuestUsername,
|
|
@@ -8119,6 +8327,7 @@ init_core();
|
|
|
8119
8327
|
computeSpread,
|
|
8120
8328
|
computeStrategyGradient,
|
|
8121
8329
|
createOrchestrationContext,
|
|
8330
|
+
diversityRerank,
|
|
8122
8331
|
docIsDeleted,
|
|
8123
8332
|
getCardHistoryID,
|
|
8124
8333
|
getCardOrigin,
|