@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/core/index.d.cts
CHANGED
|
@@ -304,6 +304,73 @@ 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
|
+
|
|
343
|
+
/**
|
|
344
|
+
* A navigation pipeline that runs a generator and applies filters sequentially.
|
|
345
|
+
*
|
|
346
|
+
* Implements StudyContentSource for backward compatibility with SessionController.
|
|
347
|
+
*
|
|
348
|
+
* ## Usage
|
|
349
|
+
*
|
|
350
|
+
* ```typescript
|
|
351
|
+
* const pipeline = new Pipeline(
|
|
352
|
+
* compositeGenerator, // or single generator
|
|
353
|
+
* [eloDistanceFilter, interferenceFilter],
|
|
354
|
+
* user,
|
|
355
|
+
* course
|
|
356
|
+
* );
|
|
357
|
+
*
|
|
358
|
+
* const cards = await pipeline.getWeightedCards(20);
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
/**
|
|
362
|
+
* Narrow capability surface for out-of-band, commit-free reads against a live
|
|
363
|
+
* pipeline (see {@link getActivePipeline}). Kept minimal on purpose — consumers
|
|
364
|
+
* get the forecast capability, not the whole `Pipeline` class.
|
|
365
|
+
*/
|
|
366
|
+
interface PipelineForecaster {
|
|
367
|
+
forecast(opts?: {
|
|
368
|
+
hints?: ReplanHints;
|
|
369
|
+
unseenOnly?: boolean;
|
|
370
|
+
threshold?: number;
|
|
371
|
+
limit?: number;
|
|
372
|
+
}): Promise<WeightedCard[]>;
|
|
373
|
+
}
|
|
307
374
|
/**
|
|
308
375
|
* Diagnosis of the full card space for the current user.
|
|
309
376
|
*/
|
|
@@ -325,6 +392,13 @@ interface CardSpaceDiagnosis {
|
|
|
325
392
|
elapsedMs: number;
|
|
326
393
|
}
|
|
327
394
|
|
|
395
|
+
/**
|
|
396
|
+
* The most recently constructed pipeline for the current session, or null if
|
|
397
|
+
* none has been built yet. This is the supported, non-debug accessor for
|
|
398
|
+
* out-of-band reads against the live pipeline (e.g. a commit-free
|
|
399
|
+
* `forecast()`), replacing reach-ins through `window.skuilder.pipeline`.
|
|
400
|
+
*/
|
|
401
|
+
declare function getActivePipeline(): PipelineForecaster | null;
|
|
328
402
|
/**
|
|
329
403
|
* Summary of a single generator's contribution.
|
|
330
404
|
*/
|
|
@@ -706,4 +780,4 @@ declare const userDBDebugAPI: {
|
|
|
706
780
|
*/
|
|
707
781
|
declare function mountUserDBDebugger(): void;
|
|
708
782
|
|
|
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 };
|
|
783
|
+
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 PipelineForecaster, type PipelineRunReport, QuestionRecord, ReplanHints, type SignalConfig, StrategyContribution, type StrategyLearningState, type StrategyStateDoc, type StrategyStateId, UserDBInterface, UserOutcomeRecord, WeightedCard, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, diversityRerank, docIsDeleted, getActivePipeline, 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,73 @@ 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
|
+
|
|
343
|
+
/**
|
|
344
|
+
* A navigation pipeline that runs a generator and applies filters sequentially.
|
|
345
|
+
*
|
|
346
|
+
* Implements StudyContentSource for backward compatibility with SessionController.
|
|
347
|
+
*
|
|
348
|
+
* ## Usage
|
|
349
|
+
*
|
|
350
|
+
* ```typescript
|
|
351
|
+
* const pipeline = new Pipeline(
|
|
352
|
+
* compositeGenerator, // or single generator
|
|
353
|
+
* [eloDistanceFilter, interferenceFilter],
|
|
354
|
+
* user,
|
|
355
|
+
* course
|
|
356
|
+
* );
|
|
357
|
+
*
|
|
358
|
+
* const cards = await pipeline.getWeightedCards(20);
|
|
359
|
+
* ```
|
|
360
|
+
*/
|
|
361
|
+
/**
|
|
362
|
+
* Narrow capability surface for out-of-band, commit-free reads against a live
|
|
363
|
+
* pipeline (see {@link getActivePipeline}). Kept minimal on purpose — consumers
|
|
364
|
+
* get the forecast capability, not the whole `Pipeline` class.
|
|
365
|
+
*/
|
|
366
|
+
interface PipelineForecaster {
|
|
367
|
+
forecast(opts?: {
|
|
368
|
+
hints?: ReplanHints;
|
|
369
|
+
unseenOnly?: boolean;
|
|
370
|
+
threshold?: number;
|
|
371
|
+
limit?: number;
|
|
372
|
+
}): Promise<WeightedCard[]>;
|
|
373
|
+
}
|
|
307
374
|
/**
|
|
308
375
|
* Diagnosis of the full card space for the current user.
|
|
309
376
|
*/
|
|
@@ -325,6 +392,13 @@ interface CardSpaceDiagnosis {
|
|
|
325
392
|
elapsedMs: number;
|
|
326
393
|
}
|
|
327
394
|
|
|
395
|
+
/**
|
|
396
|
+
* The most recently constructed pipeline for the current session, or null if
|
|
397
|
+
* none has been built yet. This is the supported, non-debug accessor for
|
|
398
|
+
* out-of-band reads against the live pipeline (e.g. a commit-free
|
|
399
|
+
* `forecast()`), replacing reach-ins through `window.skuilder.pipeline`.
|
|
400
|
+
*/
|
|
401
|
+
declare function getActivePipeline(): PipelineForecaster | null;
|
|
328
402
|
/**
|
|
329
403
|
* Summary of a single generator's contribution.
|
|
330
404
|
*/
|
|
@@ -706,4 +780,4 @@ declare const userDBDebugAPI: {
|
|
|
706
780
|
*/
|
|
707
781
|
declare function mountUserDBDebugger(): void;
|
|
708
782
|
|
|
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 };
|
|
783
|
+
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 PipelineForecaster, type PipelineRunReport, QuestionRecord, ReplanHints, type SignalConfig, StrategyContribution, type StrategyLearningState, type StrategyStateDoc, type StrategyStateId, UserDBInterface, UserOutcomeRecord, WeightedCard, aggregateOutcomesForGradient, areQuestionRecords, buildStrategyStateId, computeOutcomeSignal, computeStrategyGradient, diversityRerank, docIsDeleted, getActivePipeline, getCardHistoryID, getDefaultLearnableWeight, importParsedCards, isQuestionRecord, mountPipelineDebugger, mountUserDBDebugger, parseCardHistoryID, pipelineDebugAPI, recordUserOutcome, runPeriodUpdate, scoreAccuracyInZone, updateLearningState, updateStrategyWeight, userDBDebugAPI, validateProcessorConfig };
|
package/dist/core/index.js
CHANGED
|
@@ -719,12 +719,102 @@ 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, {
|
|
725
814
|
buildRunReport: () => buildRunReport,
|
|
726
815
|
captureRun: () => captureRun,
|
|
727
816
|
clearRunHistory: () => clearRunHistory,
|
|
817
|
+
getActivePipeline: () => getActivePipeline,
|
|
728
818
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
729
819
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
730
820
|
registerPipelineForDebug: () => registerPipelineForDebug
|
|
@@ -732,6 +822,9 @@ __export(PipelineDebugger_exports, {
|
|
|
732
822
|
function registerPipelineForDebug(pipeline) {
|
|
733
823
|
_activePipeline = pipeline;
|
|
734
824
|
}
|
|
825
|
+
function getActivePipeline() {
|
|
826
|
+
return _activePipeline;
|
|
827
|
+
}
|
|
735
828
|
function clearRunHistory() {
|
|
736
829
|
runHistory.length = 0;
|
|
737
830
|
}
|
|
@@ -1914,7 +2007,7 @@ function shuffleInPlace(arr) {
|
|
|
1914
2007
|
function pickTopByScore(cards, limit) {
|
|
1915
2008
|
return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
|
|
1916
2009
|
}
|
|
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;
|
|
2010
|
+
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
2011
|
var init_prescribed = __esm({
|
|
1919
2012
|
"src/core/navigators/generators/prescribed.ts"() {
|
|
1920
2013
|
"use strict";
|
|
@@ -1925,9 +2018,12 @@ var init_prescribed = __esm({
|
|
|
1925
2018
|
DEFAULT_MAX_SUPPORT_PER_RUN = 3;
|
|
1926
2019
|
DEFAULT_HIERARCHY_DEPTH = 2;
|
|
1927
2020
|
DEFAULT_MIN_COUNT = 3;
|
|
2021
|
+
DEFAULT_PRACTICE_MIN_COUNT = 3;
|
|
2022
|
+
DEFAULT_MAX_PRACTICE_PER_RUN = 4;
|
|
1928
2023
|
BASE_TARGET_SCORE = 1;
|
|
1929
2024
|
BASE_SUPPORT_SCORE = 0.8;
|
|
1930
2025
|
DISCOVERED_SUPPORT_SCORE = 12;
|
|
2026
|
+
BASE_PRACTICE_SCORE = 1;
|
|
1931
2027
|
MAX_TARGET_MULTIPLIER = 8;
|
|
1932
2028
|
MAX_SUPPORT_MULTIPLIER = 4;
|
|
1933
2029
|
PRESCRIBED_DEBUG_VERSION = "testversion-prescribed-v3";
|
|
@@ -2035,7 +2131,18 @@ var init_prescribed = __esm({
|
|
|
2035
2131
|
courseId,
|
|
2036
2132
|
emittedIds
|
|
2037
2133
|
);
|
|
2038
|
-
|
|
2134
|
+
const practiceCards = this.buildPracticeCards({
|
|
2135
|
+
group,
|
|
2136
|
+
courseId,
|
|
2137
|
+
emittedIds,
|
|
2138
|
+
cardsByTag,
|
|
2139
|
+
hierarchyConfigs,
|
|
2140
|
+
userTagElo,
|
|
2141
|
+
userGlobalElo,
|
|
2142
|
+
activeIds,
|
|
2143
|
+
seenIds
|
|
2144
|
+
});
|
|
2145
|
+
emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
|
|
2039
2146
|
}
|
|
2040
2147
|
const hintSummary = this.buildSupportHintSummary(groupRuntimes);
|
|
2041
2148
|
const hints = Object.keys(hintSummary.boostTags).length > 0 ? {
|
|
@@ -2063,6 +2170,7 @@ var init_prescribed = __esm({
|
|
|
2063
2170
|
const surfacedByGroup = /* @__PURE__ */ new Map();
|
|
2064
2171
|
for (const card of finalCards) {
|
|
2065
2172
|
const prov = card.provenance[0];
|
|
2173
|
+
if (prov?.reason.includes("mode=practice")) continue;
|
|
2066
2174
|
const groupId = prov?.reason.match(/group=([^;]+)/)?.[1];
|
|
2067
2175
|
const mode = prov?.reason.includes("mode=support") ? "supportIds" : "targetIds";
|
|
2068
2176
|
if (!groupId) continue;
|
|
@@ -2132,7 +2240,12 @@ var init_prescribed = __esm({
|
|
|
2132
2240
|
enabled: raw.hierarchyWalk?.enabled !== false,
|
|
2133
2241
|
maxDepth: typeof raw.hierarchyWalk?.maxDepth === "number" ? raw.hierarchyWalk.maxDepth : DEFAULT_HIERARCHY_DEPTH
|
|
2134
2242
|
},
|
|
2135
|
-
retireOnEncounter: raw.retireOnEncounter !== false
|
|
2243
|
+
retireOnEncounter: raw.retireOnEncounter !== false,
|
|
2244
|
+
practiceTagPatterns: dedupe(
|
|
2245
|
+
Array.isArray(raw.practiceTagPatterns) ? raw.practiceTagPatterns.filter((v) => typeof v === "string") : []
|
|
2246
|
+
),
|
|
2247
|
+
practiceMinCount: typeof raw.practiceMinCount === "number" ? raw.practiceMinCount : DEFAULT_PRACTICE_MIN_COUNT,
|
|
2248
|
+
maxPracticeCardsPerRun: typeof raw.maxPracticeCardsPerRun === "number" ? raw.maxPracticeCardsPerRun : DEFAULT_MAX_PRACTICE_PER_RUN
|
|
2136
2249
|
})).filter((g) => g.targetCardIds.length > 0);
|
|
2137
2250
|
return { groups };
|
|
2138
2251
|
} catch {
|
|
@@ -2355,6 +2468,92 @@ var init_prescribed = __esm({
|
|
|
2355
2468
|
}
|
|
2356
2469
|
return cards;
|
|
2357
2470
|
}
|
|
2471
|
+
/**
|
|
2472
|
+
* Emit drill cards for *unlocked-but-under-practiced* skills.
|
|
2473
|
+
*
|
|
2474
|
+
* For each course tag matching the group's `practiceTagPatterns` that is both
|
|
2475
|
+
* unlocked (all hierarchy prerequisites met — i.e. the learner has been
|
|
2476
|
+
* introduced to it) and under-practiced (per-tag attempt count below
|
|
2477
|
+
* `practiceMinCount`), this resolves cards carrying that tag and emits them
|
|
2478
|
+
* into the candidate pool. It exists because global-ELO retrieval
|
|
2479
|
+
* systematically fails to fetch the (low-ELO) drill cards for a
|
|
2480
|
+
* freshly-introduced skill — putting them in the pool here lets the pipeline's
|
|
2481
|
+
* scoring + the durable per-skill boost order them. Ordering/emphasis is NOT
|
|
2482
|
+
* this method's job; it only guarantees presence.
|
|
2483
|
+
*
|
|
2484
|
+
* Fully data-driven: the unlock relation comes from the hierarchy config and
|
|
2485
|
+
* practice-status from per-tag ELO. No card-id or tag-namespace hard-coding.
|
|
2486
|
+
*/
|
|
2487
|
+
buildPracticeCards(args) {
|
|
2488
|
+
const {
|
|
2489
|
+
group,
|
|
2490
|
+
courseId,
|
|
2491
|
+
emittedIds,
|
|
2492
|
+
cardsByTag,
|
|
2493
|
+
hierarchyConfigs,
|
|
2494
|
+
userTagElo,
|
|
2495
|
+
userGlobalElo,
|
|
2496
|
+
activeIds,
|
|
2497
|
+
seenIds
|
|
2498
|
+
} = args;
|
|
2499
|
+
const patterns = group.practiceTagPatterns ?? [];
|
|
2500
|
+
if (patterns.length === 0) return [];
|
|
2501
|
+
const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
|
|
2502
|
+
const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
|
|
2503
|
+
const practiceTags = [...cardsByTag.keys()].filter(
|
|
2504
|
+
(tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
|
|
2505
|
+
);
|
|
2506
|
+
if (practiceTags.length === 0) return [];
|
|
2507
|
+
const practiceCardIds = this.findDiscoveredSupportCards({
|
|
2508
|
+
supportTags: practiceTags,
|
|
2509
|
+
cardsByTag,
|
|
2510
|
+
activeIds,
|
|
2511
|
+
seenIds,
|
|
2512
|
+
excludedIds: emittedIds,
|
|
2513
|
+
limit: maxPractice
|
|
2514
|
+
});
|
|
2515
|
+
if (practiceCardIds.length === 0) return [];
|
|
2516
|
+
logger.info(
|
|
2517
|
+
`[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
|
|
2518
|
+
);
|
|
2519
|
+
const cards = [];
|
|
2520
|
+
for (const cardId of practiceCardIds) {
|
|
2521
|
+
emittedIds.add(cardId);
|
|
2522
|
+
cards.push({
|
|
2523
|
+
cardId,
|
|
2524
|
+
courseId,
|
|
2525
|
+
score: BASE_PRACTICE_SCORE,
|
|
2526
|
+
provenance: [
|
|
2527
|
+
{
|
|
2528
|
+
strategy: "prescribed",
|
|
2529
|
+
strategyName: this.strategyName || this.name,
|
|
2530
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
2531
|
+
action: "generated",
|
|
2532
|
+
score: BASE_PRACTICE_SCORE,
|
|
2533
|
+
reason: `mode=practice;group=${group.id};underPracticedSkills=${practiceTags.length};practiceTags=${practiceTags.slice(0, 8).join("|")}${practiceTags.length > 8 ? "|\u2026" : ""};testversion=${PRESCRIBED_DEBUG_VERSION}`
|
|
2534
|
+
}
|
|
2535
|
+
]
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
return cards;
|
|
2539
|
+
}
|
|
2540
|
+
/**
|
|
2541
|
+
* True for a skill that was *gated and is now reached*: it has at least one
|
|
2542
|
+
* declared hierarchy prerequisite set, and every set is fully satisfied by the
|
|
2543
|
+
* learner's per-tag ELO. This deliberately EXCLUDES tags with no prerequisites
|
|
2544
|
+
* — an ungated tag was never "introduced" in the curricular sense, so it isn't
|
|
2545
|
+
* a post-intro drill target (e.g. whole-word spelling tags that share the
|
|
2546
|
+
* `gpc:exercise:*` prefix but have no intro gate). Those are left to normal
|
|
2547
|
+
* ELO retrieval. This is the precise population the retrieval gap strands:
|
|
2548
|
+
* just-unlocked, low-ELO skills.
|
|
2549
|
+
*/
|
|
2550
|
+
isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) {
|
|
2551
|
+
const prereqSets = hierarchyConfigs.map((hierarchy) => hierarchy.prerequisites[tag]).filter((prereqs) => Array.isArray(prereqs) && prereqs.length > 0);
|
|
2552
|
+
if (prereqSets.length === 0) return false;
|
|
2553
|
+
return prereqSets.every(
|
|
2554
|
+
(prereqs) => prereqs.every((pr) => this.isPrerequisiteMet(pr, userTagElo[pr.tag], userGlobalElo))
|
|
2555
|
+
);
|
|
2556
|
+
}
|
|
2358
2557
|
findSupportCardsByTags(group, tagsByCard, supportTags) {
|
|
2359
2558
|
if (supportTags.length === 0) {
|
|
2360
2559
|
return [];
|
|
@@ -4148,7 +4347,7 @@ function logResultCards(cards) {
|
|
|
4148
4347
|
for (let i = 0; i < cards.length; i++) {
|
|
4149
4348
|
const c = cards[i];
|
|
4150
4349
|
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) => {
|
|
4350
|
+
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
4351
|
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
4153
4352
|
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
4154
4353
|
}).join(" | ");
|
|
@@ -4180,6 +4379,7 @@ var init_Pipeline = __esm({
|
|
|
4180
4379
|
init_logger();
|
|
4181
4380
|
init_orchestration();
|
|
4182
4381
|
init_PipelineDebugger();
|
|
4382
|
+
init_diversityRerank();
|
|
4183
4383
|
VERBOSE_RESULTS = true;
|
|
4184
4384
|
Pipeline = class extends ContentNavigator {
|
|
4185
4385
|
generator;
|
|
@@ -4353,6 +4553,7 @@ var init_Pipeline = __esm({
|
|
|
4353
4553
|
this._ephemeralHints = null;
|
|
4354
4554
|
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
4355
4555
|
}
|
|
4556
|
+
cards = diversityRerank(cards);
|
|
4356
4557
|
cards.sort((a, b) => b.score - a.score);
|
|
4357
4558
|
const tFilter = performance.now();
|
|
4358
4559
|
const result = cards.slice(0, limit);
|
|
@@ -4656,6 +4857,68 @@ var init_Pipeline = __esm({
|
|
|
4656
4857
|
// ---------------------------------------------------------------------------
|
|
4657
4858
|
// Card-space diagnostic
|
|
4658
4859
|
// ---------------------------------------------------------------------------
|
|
4860
|
+
/**
|
|
4861
|
+
* Commit-free forecast: score the user's full card space through the filter
|
|
4862
|
+
* chain and return the cards that are currently *reachable* (score >=
|
|
4863
|
+
* threshold), optionally nudged by caller-supplied hints and/or restricted
|
|
4864
|
+
* to cards the user hasn't seen yet.
|
|
4865
|
+
*
|
|
4866
|
+
* This is a GENERIC primitive — it returns scored, tag-hydrated cards and
|
|
4867
|
+
* stops there. It has no knowledge of any particular tag convention; callers
|
|
4868
|
+
* decide what the surviving cards mean (e.g. filter to their own "intro"
|
|
4869
|
+
* tag family). Nothing is written and no session is started.
|
|
4870
|
+
*
|
|
4871
|
+
* The optional `hints` are the "out-of-band kick": they run through the same
|
|
4872
|
+
* {@link applyHints} path a live replan uses, so the two semantics carry over —
|
|
4873
|
+
* - `boostTags`/`boostCards` reweight *within* gating (a gated score-0 card
|
|
4874
|
+
* stays out), and
|
|
4875
|
+
* - `requireTags`/`requireCards` inject from the full pre-filter pool,
|
|
4876
|
+
* *bypassing* gating (use when you want a card regardless of reachability).
|
|
4877
|
+
* Note `unseenOnly` is applied LAST, so it can drop a `require`d card that the
|
|
4878
|
+
* user has already seen — pass `unseenOnly: false` if that matters.
|
|
4879
|
+
*
|
|
4880
|
+
* Cost note: like {@link diagnoseCardSpace}, this scans every card through the
|
|
4881
|
+
* filters, so it's heavier than a normal replan. Intended for one-shot
|
|
4882
|
+
* out-of-band use (e.g. a session-end "what's next" snapshot), not the hot path.
|
|
4883
|
+
*
|
|
4884
|
+
* @param opts.hints Optional ephemeral hints to apply after the filter chain.
|
|
4885
|
+
* @param opts.unseenOnly Only return cards the user hasn't encountered (default true).
|
|
4886
|
+
* @param opts.threshold Min score to count as reachable (default 0.10).
|
|
4887
|
+
* @param opts.limit Optional cap on results (already sorted desc).
|
|
4888
|
+
*/
|
|
4889
|
+
async forecast(opts) {
|
|
4890
|
+
const threshold = opts?.threshold ?? 0.1;
|
|
4891
|
+
const unseenOnly = opts?.unseenOnly ?? true;
|
|
4892
|
+
const courseId = this.course.getCourseID();
|
|
4893
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
4894
|
+
let cards = allCardIds.map((cardId) => ({
|
|
4895
|
+
cardId,
|
|
4896
|
+
courseId,
|
|
4897
|
+
score: 1,
|
|
4898
|
+
provenance: []
|
|
4899
|
+
}));
|
|
4900
|
+
cards = await this.hydrateTags(cards);
|
|
4901
|
+
const fullPool = cards.slice();
|
|
4902
|
+
const context = await this.buildContext();
|
|
4903
|
+
for (const filter of this.filters) {
|
|
4904
|
+
cards = await filter.transform(cards, context);
|
|
4905
|
+
}
|
|
4906
|
+
if (opts?.hints) {
|
|
4907
|
+
cards = this.applyHints(cards, opts.hints, fullPool);
|
|
4908
|
+
}
|
|
4909
|
+
cards = cards.filter((c) => c.score >= threshold);
|
|
4910
|
+
if (unseenOnly) {
|
|
4911
|
+
let encountered;
|
|
4912
|
+
try {
|
|
4913
|
+
encountered = new Set(await this.user.getSeenCards(courseId));
|
|
4914
|
+
} catch {
|
|
4915
|
+
encountered = /* @__PURE__ */ new Set();
|
|
4916
|
+
}
|
|
4917
|
+
cards = cards.filter((c) => !encountered.has(c.cardId));
|
|
4918
|
+
}
|
|
4919
|
+
cards.sort((a, b) => b.score - a.score);
|
|
4920
|
+
return opts?.limit ? cards.slice(0, opts.limit) : cards;
|
|
4921
|
+
}
|
|
4659
4922
|
/**
|
|
4660
4923
|
* Scan every card in the course through the filter chain and report
|
|
4661
4924
|
* how many are "well indicated" (score >= threshold) for the current user.
|
|
@@ -4921,6 +5184,7 @@ var init_3 = __esm({
|
|
|
4921
5184
|
"./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
|
|
4922
5185
|
"./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
|
|
4923
5186
|
"./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
|
|
5187
|
+
"./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
|
|
4924
5188
|
"./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
|
|
4925
5189
|
"./filters/eloDistance.ts": () => Promise.resolve().then(() => (init_eloDistance(), eloDistance_exports)),
|
|
4926
5190
|
"./filters/hierarchyDefinition.ts": () => Promise.resolve().then(() => (init_hierarchyDefinition(), hierarchyDefinition_exports)),
|
|
@@ -4946,9 +5210,13 @@ var init_3 = __esm({
|
|
|
4946
5210
|
var navigators_exports = {};
|
|
4947
5211
|
__export(navigators_exports, {
|
|
4948
5212
|
ContentNavigator: () => ContentNavigator,
|
|
5213
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
5214
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
4949
5215
|
NavigatorRole: () => NavigatorRole,
|
|
4950
5216
|
NavigatorRoles: () => NavigatorRoles,
|
|
4951
5217
|
Navigators: () => Navigators,
|
|
5218
|
+
diversityRerank: () => diversityRerank,
|
|
5219
|
+
getActivePipeline: () => getActivePipeline,
|
|
4952
5220
|
getCardOrigin: () => getCardOrigin,
|
|
4953
5221
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
4954
5222
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
@@ -5032,6 +5300,7 @@ var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigat
|
|
|
5032
5300
|
var init_navigators = __esm({
|
|
5033
5301
|
"src/core/navigators/index.ts"() {
|
|
5034
5302
|
"use strict";
|
|
5303
|
+
init_diversityRerank();
|
|
5035
5304
|
init_PipelineDebugger();
|
|
5036
5305
|
init_logger();
|
|
5037
5306
|
init_();
|
|
@@ -8039,6 +8308,8 @@ Examples:
|
|
|
8039
8308
|
var core_exports = {};
|
|
8040
8309
|
__export(core_exports, {
|
|
8041
8310
|
ContentNavigator: () => ContentNavigator,
|
|
8311
|
+
DIVERSITY_FLOOR: () => DIVERSITY_FLOOR,
|
|
8312
|
+
DIVERSITY_STRENGTH: () => DIVERSITY_STRENGTH,
|
|
8042
8313
|
DocType: () => DocType,
|
|
8043
8314
|
DocTypePrefixes: () => DocTypePrefixes,
|
|
8044
8315
|
GuestUsername: () => GuestUsername,
|
|
@@ -8055,7 +8326,9 @@ __export(core_exports, {
|
|
|
8055
8326
|
computeSpread: () => computeSpread,
|
|
8056
8327
|
computeStrategyGradient: () => computeStrategyGradient,
|
|
8057
8328
|
createOrchestrationContext: () => createOrchestrationContext,
|
|
8329
|
+
diversityRerank: () => diversityRerank,
|
|
8058
8330
|
docIsDeleted: () => docIsDeleted,
|
|
8331
|
+
getActivePipeline: () => getActivePipeline,
|
|
8059
8332
|
getCardHistoryID: () => getCardHistoryID,
|
|
8060
8333
|
getCardOrigin: () => getCardOrigin,
|
|
8061
8334
|
getDefaultLearnableWeight: () => getDefaultLearnableWeight,
|
|
@@ -8104,6 +8377,8 @@ init_core();
|
|
|
8104
8377
|
// Annotate the CommonJS export names for ESM import in node:
|
|
8105
8378
|
0 && (module.exports = {
|
|
8106
8379
|
ContentNavigator,
|
|
8380
|
+
DIVERSITY_FLOOR,
|
|
8381
|
+
DIVERSITY_STRENGTH,
|
|
8107
8382
|
DocType,
|
|
8108
8383
|
DocTypePrefixes,
|
|
8109
8384
|
GuestUsername,
|
|
@@ -8120,7 +8395,9 @@ init_core();
|
|
|
8120
8395
|
computeSpread,
|
|
8121
8396
|
computeStrategyGradient,
|
|
8122
8397
|
createOrchestrationContext,
|
|
8398
|
+
diversityRerank,
|
|
8123
8399
|
docIsDeleted,
|
|
8400
|
+
getActivePipeline,
|
|
8124
8401
|
getCardHistoryID,
|
|
8125
8402
|
getCardOrigin,
|
|
8126
8403
|
getDefaultLearnableWeight,
|