@vue-skuilder/db 0.1.31-a → 0.1.31
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/{contentSource-BmnmvH8C.d.ts → contentSource-Bdwkvqa8.d.ts} +35 -4
- package/dist/{contentSource-DfBbaLA-.d.cts → contentSource-DF1nUbPQ.d.cts} +35 -4
- package/dist/core/index.d.cts +48 -3
- package/dist/core/index.d.ts +48 -3
- package/dist/core/index.js +587 -56
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +586 -56
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BeRXVMs5.d.cts → dataLayerProvider-BKmVoyJR.d.ts} +20 -1
- package/dist/{dataLayerProvider-CG9GfaAY.d.ts → dataLayerProvider-BQdfJuBN.d.cts} +20 -1
- package/dist/impl/couch/index.d.cts +156 -4
- package/dist/impl/couch/index.d.ts +156 -4
- package/dist/impl/couch/index.js +805 -47
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +804 -47
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +3 -2
- package/dist/impl/static/index.d.ts +3 -2
- package/dist/impl/static/index.js +542 -37
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +542 -37
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +64 -3
- package/dist/index.d.ts +64 -3
- package/dist/index.js +1040 -90
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1030 -81
- package/dist/index.mjs.map +1 -1
- package/docs/navigators-architecture.md +64 -5
- package/package.json +3 -3
- package/src/core/interfaces/contentSource.ts +6 -0
- package/src/core/interfaces/courseDB.ts +6 -0
- package/src/core/interfaces/dataLayerProvider.ts +20 -0
- package/src/core/navigators/Pipeline.ts +414 -9
- package/src/core/navigators/PipelineAssembler.ts +23 -18
- package/src/core/navigators/PipelineDebugger.ts +115 -1
- package/src/core/navigators/filters/hierarchyDefinition.ts +78 -8
- package/src/core/navigators/generators/prescribed.ts +95 -0
- package/src/core/navigators/index.ts +55 -10
- package/src/impl/common/BaseUserDB.ts +4 -1
- package/src/impl/couch/CourseSyncService.ts +356 -0
- package/src/impl/couch/PouchDataLayerProvider.ts +21 -1
- package/src/impl/couch/courseDB.ts +60 -13
- package/src/impl/couch/index.ts +1 -0
- package/src/impl/static/courseDB.ts +5 -0
- package/src/study/ItemQueue.ts +42 -0
- package/src/study/SessionController.ts +195 -22
- package/src/study/SpacedRepetition.ts +7 -2
- package/tests/core/navigators/Pipeline.test.ts +1 -1
- package/tests/core/navigators/PipelineAssembler.test.ts +15 -14
|
@@ -528,8 +528,12 @@ __export(PipelineDebugger_exports, {
|
|
|
528
528
|
buildRunReport: () => buildRunReport,
|
|
529
529
|
captureRun: () => captureRun,
|
|
530
530
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
531
|
-
pipelineDebugAPI: () => pipelineDebugAPI
|
|
531
|
+
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
532
|
+
registerPipelineForDebug: () => registerPipelineForDebug
|
|
532
533
|
});
|
|
534
|
+
function registerPipelineForDebug(pipeline) {
|
|
535
|
+
_activePipeline = pipeline;
|
|
536
|
+
}
|
|
533
537
|
function getOrigin(card) {
|
|
534
538
|
const firstEntry = card.provenance[0];
|
|
535
539
|
if (!firstEntry) return "unknown";
|
|
@@ -557,6 +561,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
557
561
|
origin: getOrigin(card),
|
|
558
562
|
finalScore: card.score,
|
|
559
563
|
provenance: card.provenance,
|
|
564
|
+
tags: card.tags,
|
|
560
565
|
selected: selectedIds.has(card.cardId)
|
|
561
566
|
}));
|
|
562
567
|
const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
|
|
@@ -612,11 +617,13 @@ function mountPipelineDebugger() {
|
|
|
612
617
|
win.skuilder = win.skuilder || {};
|
|
613
618
|
win.skuilder.pipeline = pipelineDebugAPI;
|
|
614
619
|
}
|
|
615
|
-
var MAX_RUNS, runHistory, pipelineDebugAPI;
|
|
620
|
+
var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
|
|
616
621
|
var init_PipelineDebugger = __esm({
|
|
617
622
|
"src/core/navigators/PipelineDebugger.ts"() {
|
|
618
623
|
"use strict";
|
|
624
|
+
init_navigators();
|
|
619
625
|
init_logger();
|
|
626
|
+
_activePipeline = null;
|
|
620
627
|
MAX_RUNS = 10;
|
|
621
628
|
runHistory = [];
|
|
622
629
|
pipelineDebugAPI = {
|
|
@@ -758,6 +765,81 @@ var init_PipelineDebugger = __esm({
|
|
|
758
765
|
runHistory.length = 0;
|
|
759
766
|
logger.info("[Pipeline Debug] Run history cleared.");
|
|
760
767
|
},
|
|
768
|
+
/**
|
|
769
|
+
* Show the navigator registry: all registered classes and their roles.
|
|
770
|
+
*
|
|
771
|
+
* Useful for verifying that consumer-defined navigators were registered
|
|
772
|
+
* before pipeline assembly.
|
|
773
|
+
*/
|
|
774
|
+
showRegistry() {
|
|
775
|
+
const names = getRegisteredNavigatorNames();
|
|
776
|
+
if (names.length === 0) {
|
|
777
|
+
logger.info("[Pipeline Debug] Navigator registry is empty.");
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
console.group("\u{1F4E6} Navigator Registry");
|
|
781
|
+
console.table(
|
|
782
|
+
names.map((name) => {
|
|
783
|
+
const registryRole = getRegisteredNavigatorRole(name);
|
|
784
|
+
const builtinRole = NavigatorRoles[name];
|
|
785
|
+
const effectiveRole = builtinRole || registryRole || "\u26A0\uFE0F NONE";
|
|
786
|
+
const source = builtinRole ? "built-in" : registryRole ? "consumer" : "unclassified";
|
|
787
|
+
return {
|
|
788
|
+
name,
|
|
789
|
+
role: effectiveRole,
|
|
790
|
+
source,
|
|
791
|
+
isGenerator: isGenerator(name),
|
|
792
|
+
isFilter: isFilter(name)
|
|
793
|
+
};
|
|
794
|
+
})
|
|
795
|
+
);
|
|
796
|
+
console.groupEnd();
|
|
797
|
+
},
|
|
798
|
+
/**
|
|
799
|
+
* Show strategy documents from the last pipeline run and how they mapped
|
|
800
|
+
* to the registry.
|
|
801
|
+
*
|
|
802
|
+
* If no runs are captured yet, falls back to showing just the registry.
|
|
803
|
+
*/
|
|
804
|
+
showStrategies() {
|
|
805
|
+
this.showRegistry();
|
|
806
|
+
if (runHistory.length === 0) {
|
|
807
|
+
logger.info("[Pipeline Debug] No pipeline runs captured yet \u2014 cannot show strategy doc mapping.");
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const run = runHistory[0];
|
|
811
|
+
console.group("\u{1F50C} Pipeline Strategy Mapping (last run)");
|
|
812
|
+
logger.info(`Generator: ${run.generatorName}`);
|
|
813
|
+
if (run.generators && run.generators.length > 0) {
|
|
814
|
+
for (const g of run.generators) {
|
|
815
|
+
logger.info(` \u{1F4E5} ${g.name}: ${g.cardCount} cards (${g.newCount} new, ${g.reviewCount} reviews)`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (run.filters.length > 0) {
|
|
819
|
+
logger.info("Filters:");
|
|
820
|
+
for (const f of run.filters) {
|
|
821
|
+
logger.info(` \u{1F538} ${f.name}: \u2191${f.boosted} \u2193${f.penalized} =${f.passed} \u2715${f.removed}`);
|
|
822
|
+
}
|
|
823
|
+
} else {
|
|
824
|
+
logger.info("Filters: (none)");
|
|
825
|
+
}
|
|
826
|
+
console.groupEnd();
|
|
827
|
+
},
|
|
828
|
+
/**
|
|
829
|
+
* Scan the full card space through the filter chain for the current user.
|
|
830
|
+
*
|
|
831
|
+
* Reports how many cards are well-indicated and how many are new.
|
|
832
|
+
* Use this to understand how the search space grows during onboarding.
|
|
833
|
+
*
|
|
834
|
+
* @param threshold - Score threshold for "well indicated" (default 0.10)
|
|
835
|
+
*/
|
|
836
|
+
async diagnoseCardSpace(threshold) {
|
|
837
|
+
if (!_activePipeline) {
|
|
838
|
+
logger.info("[Pipeline Debug] No active pipeline. Run a session first.");
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
return _activePipeline.diagnoseCardSpace({ threshold });
|
|
842
|
+
},
|
|
761
843
|
/**
|
|
762
844
|
* Show help.
|
|
763
845
|
*/
|
|
@@ -770,6 +852,9 @@ Commands:
|
|
|
770
852
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
771
853
|
.showCard(cardId) Show provenance trail for a specific card
|
|
772
854
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
855
|
+
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
856
|
+
.showRegistry() Show navigator registry (classes + roles)
|
|
857
|
+
.showStrategies() Show registry + strategy mapping from last run
|
|
773
858
|
.listRuns() List all captured runs in table format
|
|
774
859
|
.export() Export run history as JSON for bug reports
|
|
775
860
|
.clear() Clear run history
|
|
@@ -779,7 +864,7 @@ Commands:
|
|
|
779
864
|
Example:
|
|
780
865
|
window.skuilder.pipeline.showLastRun()
|
|
781
866
|
window.skuilder.pipeline.showRun(1)
|
|
782
|
-
window.skuilder.pipeline.
|
|
867
|
+
await window.skuilder.pipeline.diagnoseCardSpace()
|
|
783
868
|
`);
|
|
784
869
|
}
|
|
785
870
|
};
|
|
@@ -1074,6 +1159,69 @@ var init_generators = __esm({
|
|
|
1074
1159
|
}
|
|
1075
1160
|
});
|
|
1076
1161
|
|
|
1162
|
+
// src/core/navigators/generators/prescribed.ts
|
|
1163
|
+
var prescribed_exports = {};
|
|
1164
|
+
__export(prescribed_exports, {
|
|
1165
|
+
default: () => PrescribedCardsGenerator
|
|
1166
|
+
});
|
|
1167
|
+
var PrescribedCardsGenerator;
|
|
1168
|
+
var init_prescribed = __esm({
|
|
1169
|
+
"src/core/navigators/generators/prescribed.ts"() {
|
|
1170
|
+
"use strict";
|
|
1171
|
+
init_navigators();
|
|
1172
|
+
init_logger();
|
|
1173
|
+
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1174
|
+
name;
|
|
1175
|
+
config;
|
|
1176
|
+
constructor(user, course, strategyData) {
|
|
1177
|
+
super(user, course, strategyData);
|
|
1178
|
+
this.name = strategyData.name || "Prescribed Cards";
|
|
1179
|
+
try {
|
|
1180
|
+
const parsed = JSON.parse(strategyData.serializedData);
|
|
1181
|
+
this.config = { cardIds: parsed.cardIds || [] };
|
|
1182
|
+
} catch {
|
|
1183
|
+
this.config = { cardIds: [] };
|
|
1184
|
+
}
|
|
1185
|
+
logger.debug(
|
|
1186
|
+
`[Prescribed] Initialized with ${this.config.cardIds.length} prescribed cards`
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1189
|
+
async getWeightedCards(limit, _context) {
|
|
1190
|
+
if (this.config.cardIds.length === 0) {
|
|
1191
|
+
return [];
|
|
1192
|
+
}
|
|
1193
|
+
const courseId = this.course.getCourseID();
|
|
1194
|
+
const activeCards = await this.user.getActiveCards();
|
|
1195
|
+
const activeIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
1196
|
+
const eligibleIds = this.config.cardIds.filter((id) => !activeIds.has(id));
|
|
1197
|
+
if (eligibleIds.length === 0) {
|
|
1198
|
+
logger.debug("[Prescribed] All prescribed cards already active, returning empty");
|
|
1199
|
+
return [];
|
|
1200
|
+
}
|
|
1201
|
+
const cards = eligibleIds.slice(0, limit).map((cardId) => ({
|
|
1202
|
+
cardId,
|
|
1203
|
+
courseId,
|
|
1204
|
+
score: 1,
|
|
1205
|
+
provenance: [
|
|
1206
|
+
{
|
|
1207
|
+
strategy: "prescribed",
|
|
1208
|
+
strategyName: this.strategyName || this.name,
|
|
1209
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1210
|
+
action: "generated",
|
|
1211
|
+
score: 1,
|
|
1212
|
+
reason: `Prescribed card (${eligibleIds.length} eligible of ${this.config.cardIds.length} configured)`
|
|
1213
|
+
}
|
|
1214
|
+
]
|
|
1215
|
+
}));
|
|
1216
|
+
logger.info(
|
|
1217
|
+
`[Prescribed] Emitting ${cards.length} cards (${eligibleIds.length} eligible, ${activeIds.size} already active)`
|
|
1218
|
+
);
|
|
1219
|
+
return cards;
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1077
1225
|
// src/core/navigators/generators/srs.ts
|
|
1078
1226
|
var srs_exports = {};
|
|
1079
1227
|
__export(srs_exports, {
|
|
@@ -1268,6 +1416,7 @@ var init_ = __esm({
|
|
|
1268
1416
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
1269
1417
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
1270
1418
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
1419
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
1271
1420
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
1272
1421
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports))
|
|
1273
1422
|
});
|
|
@@ -1468,6 +1617,8 @@ var init_hierarchyDefinition = __esm({
|
|
|
1468
1617
|
if (userTagElo.count < minCount) return false;
|
|
1469
1618
|
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1470
1619
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
1620
|
+
} else if (prereq.masteryThreshold?.minCount !== void 0) {
|
|
1621
|
+
return true;
|
|
1471
1622
|
} else {
|
|
1472
1623
|
return userTagElo.score >= userGlobalElo;
|
|
1473
1624
|
}
|
|
@@ -1543,14 +1694,38 @@ var init_hierarchyDefinition = __esm({
|
|
|
1543
1694
|
};
|
|
1544
1695
|
}
|
|
1545
1696
|
}
|
|
1697
|
+
/**
|
|
1698
|
+
* Build a map of prereq tag → max configured boost for all *closed* gates.
|
|
1699
|
+
*
|
|
1700
|
+
* When a gate is closed (prereqs unmet), cards carrying that gate's prereq
|
|
1701
|
+
* tags get boosted — steering the pipeline toward content that helps unlock
|
|
1702
|
+
* the gated material. Once the gate opens, the boost disappears.
|
|
1703
|
+
*/
|
|
1704
|
+
getPreReqBoosts(unlockedTags, masteredTags) {
|
|
1705
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
1706
|
+
for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
|
|
1707
|
+
if (unlockedTags.has(tagId)) continue;
|
|
1708
|
+
for (const prereq of prereqs) {
|
|
1709
|
+
if (!prereq.preReqBoost || prereq.preReqBoost <= 1) continue;
|
|
1710
|
+
if (masteredTags.has(prereq.tag)) continue;
|
|
1711
|
+
const existing = boosts.get(prereq.tag) ?? 1;
|
|
1712
|
+
boosts.set(prereq.tag, Math.max(existing, prereq.preReqBoost));
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
return boosts;
|
|
1716
|
+
}
|
|
1546
1717
|
/**
|
|
1547
1718
|
* CardFilter.transform implementation.
|
|
1548
1719
|
*
|
|
1549
|
-
*
|
|
1720
|
+
* Two effects:
|
|
1721
|
+
* 1. Cards with locked tags receive score * 0.05 (gating penalty)
|
|
1722
|
+
* 2. Cards carrying prereq tags of closed gates receive a configured
|
|
1723
|
+
* boost (preReqBoost), steering toward content that unlocks gates
|
|
1550
1724
|
*/
|
|
1551
1725
|
async transform(cards, context) {
|
|
1552
1726
|
const masteredTags = await this.getMasteredTags(context);
|
|
1553
1727
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
1728
|
+
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
1554
1729
|
const gated = [];
|
|
1555
1730
|
for (const card of cards) {
|
|
1556
1731
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
@@ -1559,9 +1734,27 @@ var init_hierarchyDefinition = __esm({
|
|
|
1559
1734
|
unlockedTags,
|
|
1560
1735
|
masteredTags
|
|
1561
1736
|
);
|
|
1562
|
-
const LOCKED_PENALTY = 0.
|
|
1563
|
-
|
|
1564
|
-
|
|
1737
|
+
const LOCKED_PENALTY = 0.02;
|
|
1738
|
+
let finalScore = isUnlocked ? card.score : card.score * LOCKED_PENALTY;
|
|
1739
|
+
let action = isUnlocked ? "passed" : "penalized";
|
|
1740
|
+
let finalReason = reason;
|
|
1741
|
+
if (isUnlocked && preReqBoosts.size > 0) {
|
|
1742
|
+
const cardTags = card.tags ?? [];
|
|
1743
|
+
let maxBoost = 1;
|
|
1744
|
+
const boostedPrereqs = [];
|
|
1745
|
+
for (const tag of cardTags) {
|
|
1746
|
+
const boost = preReqBoosts.get(tag);
|
|
1747
|
+
if (boost && boost > maxBoost) {
|
|
1748
|
+
maxBoost = boost;
|
|
1749
|
+
boostedPrereqs.push(tag);
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (maxBoost > 1) {
|
|
1753
|
+
finalScore *= maxBoost;
|
|
1754
|
+
action = "boosted";
|
|
1755
|
+
finalReason = `${reason} | preReqBoost \xD7${maxBoost.toFixed(2)} for ${boostedPrereqs.join(", ")}`;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1565
1758
|
gated.push({
|
|
1566
1759
|
...card,
|
|
1567
1760
|
score: finalScore,
|
|
@@ -1573,7 +1766,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1573
1766
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-hierarchy",
|
|
1574
1767
|
action,
|
|
1575
1768
|
score: finalScore,
|
|
1576
|
-
reason
|
|
1769
|
+
reason: finalReason
|
|
1577
1770
|
}
|
|
1578
1771
|
]
|
|
1579
1772
|
});
|
|
@@ -2261,6 +2454,18 @@ var Pipeline_exports = {};
|
|
|
2261
2454
|
__export(Pipeline_exports, {
|
|
2262
2455
|
Pipeline: () => Pipeline
|
|
2263
2456
|
});
|
|
2457
|
+
function globToRegex(pattern) {
|
|
2458
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
2459
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
2460
|
+
return new RegExp(`^${withWildcards}$`);
|
|
2461
|
+
}
|
|
2462
|
+
function globMatch(value, pattern) {
|
|
2463
|
+
if (!pattern.includes("*")) return value === pattern;
|
|
2464
|
+
return globToRegex(pattern).test(value);
|
|
2465
|
+
}
|
|
2466
|
+
function cardMatchesTagPattern(card, pattern) {
|
|
2467
|
+
return (card.tags ?? []).some((tag) => globMatch(tag, pattern));
|
|
2468
|
+
}
|
|
2264
2469
|
function logPipelineConfig(generator, filters) {
|
|
2265
2470
|
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
2266
2471
|
logger.info(
|
|
@@ -2295,6 +2500,21 @@ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCo
|
|
|
2295
2500
|
\u{1F4A1} Inspect: window.skuilder.pipeline`
|
|
2296
2501
|
);
|
|
2297
2502
|
}
|
|
2503
|
+
function logResultCards(cards) {
|
|
2504
|
+
if (!VERBOSE_RESULTS || cards.length === 0) return;
|
|
2505
|
+
logger.info(`[Pipeline] Results (${cards.length} cards):`);
|
|
2506
|
+
for (let i = 0; i < cards.length; i++) {
|
|
2507
|
+
const c = cards[i];
|
|
2508
|
+
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
2509
|
+
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
2510
|
+
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
2511
|
+
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
2512
|
+
}).join(" | ");
|
|
2513
|
+
logger.info(
|
|
2514
|
+
`[Pipeline] ${String(i + 1).padStart(2)}. ${c.score.toFixed(4)} ${c.cardId} [${tags}]${filters ? ` {${filters}}` : ""}`
|
|
2515
|
+
);
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2298
2518
|
function logCardProvenance(cards, maxCards = 3) {
|
|
2299
2519
|
const cardsToLog = cards.slice(0, maxCards);
|
|
2300
2520
|
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
@@ -2309,7 +2529,7 @@ function logCardProvenance(cards, maxCards = 3) {
|
|
|
2309
2529
|
}
|
|
2310
2530
|
}
|
|
2311
2531
|
}
|
|
2312
|
-
var import_common8, Pipeline;
|
|
2532
|
+
var import_common8, VERBOSE_RESULTS, Pipeline;
|
|
2313
2533
|
var init_Pipeline = __esm({
|
|
2314
2534
|
"src/core/navigators/Pipeline.ts"() {
|
|
2315
2535
|
"use strict";
|
|
@@ -2318,9 +2538,31 @@ var init_Pipeline = __esm({
|
|
|
2318
2538
|
init_logger();
|
|
2319
2539
|
init_orchestration();
|
|
2320
2540
|
init_PipelineDebugger();
|
|
2541
|
+
VERBOSE_RESULTS = true;
|
|
2321
2542
|
Pipeline = class extends ContentNavigator {
|
|
2322
2543
|
generator;
|
|
2323
2544
|
filters;
|
|
2545
|
+
/**
|
|
2546
|
+
* Cached orchestration context. Course config and salt don't change within
|
|
2547
|
+
* a page load, so we build the orchestration context once and reuse it on
|
|
2548
|
+
* subsequent getWeightedCards() calls (e.g. mid-session replans).
|
|
2549
|
+
*
|
|
2550
|
+
* This eliminates a remote getCourseConfig() round trip per pipeline run.
|
|
2551
|
+
*/
|
|
2552
|
+
_cachedOrchestration = null;
|
|
2553
|
+
/**
|
|
2554
|
+
* Persistent tag cache. Maps cardId → tag names.
|
|
2555
|
+
*
|
|
2556
|
+
* Tags are static within a session (they're set at card generation time),
|
|
2557
|
+
* so we cache them across pipeline runs. On replans, many of the same cards
|
|
2558
|
+
* reappear — cache hits avoid redundant remote getAppliedTagsBatch() queries.
|
|
2559
|
+
*/
|
|
2560
|
+
_tagCache = /* @__PURE__ */ new Map();
|
|
2561
|
+
/**
|
|
2562
|
+
* One-shot replan hints. Applied after the filter chain on the next
|
|
2563
|
+
* getWeightedCards() call, then cleared.
|
|
2564
|
+
*/
|
|
2565
|
+
_ephemeralHints = null;
|
|
2324
2566
|
/**
|
|
2325
2567
|
* Create a new pipeline.
|
|
2326
2568
|
*
|
|
@@ -2341,6 +2583,17 @@ var init_Pipeline = __esm({
|
|
|
2341
2583
|
logger.error(`[pipeline] Failed to lookup courseCfg: ${e}`);
|
|
2342
2584
|
});
|
|
2343
2585
|
logPipelineConfig(generator, filters);
|
|
2586
|
+
registerPipelineForDebug(this);
|
|
2587
|
+
}
|
|
2588
|
+
/**
|
|
2589
|
+
* Set one-shot hints for the next pipeline run.
|
|
2590
|
+
* Consumed after one getWeightedCards() call, then cleared.
|
|
2591
|
+
*
|
|
2592
|
+
* Overrides ContentNavigator.setEphemeralHints() no-op.
|
|
2593
|
+
*/
|
|
2594
|
+
setEphemeralHints(hints) {
|
|
2595
|
+
this._ephemeralHints = hints;
|
|
2596
|
+
logger.info(`[Pipeline] Ephemeral hints set: ${JSON.stringify(hints)}`);
|
|
2344
2597
|
}
|
|
2345
2598
|
/**
|
|
2346
2599
|
* Get weighted cards by running generator and applying filters.
|
|
@@ -2357,13 +2610,15 @@ var init_Pipeline = __esm({
|
|
|
2357
2610
|
* @returns Cards sorted by score descending
|
|
2358
2611
|
*/
|
|
2359
2612
|
async getWeightedCards(limit) {
|
|
2613
|
+
const t0 = performance.now();
|
|
2360
2614
|
const context = await this.buildContext();
|
|
2361
|
-
const
|
|
2362
|
-
const fetchLimit =
|
|
2615
|
+
const tContext = performance.now();
|
|
2616
|
+
const fetchLimit = 500;
|
|
2363
2617
|
logger.debug(
|
|
2364
2618
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
2365
2619
|
);
|
|
2366
2620
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
2621
|
+
const tGenerate = performance.now();
|
|
2367
2622
|
const generatedCount = cards.length;
|
|
2368
2623
|
let generatorSummaries;
|
|
2369
2624
|
if (this.generator.generators) {
|
|
@@ -2392,6 +2647,7 @@ var init_Pipeline = __esm({
|
|
|
2392
2647
|
}
|
|
2393
2648
|
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
2394
2649
|
cards = await this.hydrateTags(cards);
|
|
2650
|
+
const tHydrate = performance.now();
|
|
2395
2651
|
const allCardsBeforeFiltering = [...cards];
|
|
2396
2652
|
const filterImpacts = [];
|
|
2397
2653
|
for (const filter of this.filters) {
|
|
@@ -2410,8 +2666,17 @@ var init_Pipeline = __esm({
|
|
|
2410
2666
|
logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
|
|
2411
2667
|
}
|
|
2412
2668
|
cards = cards.filter((c) => c.score > 0);
|
|
2669
|
+
const hints = this._ephemeralHints;
|
|
2670
|
+
if (hints) {
|
|
2671
|
+
this._ephemeralHints = null;
|
|
2672
|
+
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
2673
|
+
}
|
|
2413
2674
|
cards.sort((a, b) => b.score - a.score);
|
|
2675
|
+
const tFilter = performance.now();
|
|
2414
2676
|
const result = cards.slice(0, limit);
|
|
2677
|
+
logger.info(
|
|
2678
|
+
`[Pipeline:timing] total=${(tFilter - t0).toFixed(0)}ms (context=${(tContext - t0).toFixed(0)} generate=${(tGenerate - tContext).toFixed(0)} hydrate=${(tHydrate - tGenerate).toFixed(0)} filter=${(tFilter - tHydrate).toFixed(0)})`
|
|
2679
|
+
);
|
|
2415
2680
|
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
2416
2681
|
logExecutionSummary(
|
|
2417
2682
|
this.generator.name,
|
|
@@ -2421,6 +2686,7 @@ var init_Pipeline = __esm({
|
|
|
2421
2686
|
topScores,
|
|
2422
2687
|
filterImpacts
|
|
2423
2688
|
);
|
|
2689
|
+
logResultCards(result);
|
|
2424
2690
|
logCardProvenance(result, 3);
|
|
2425
2691
|
try {
|
|
2426
2692
|
const courseName = await this.course?.getCourseConfig().then((c) => c.name).catch(() => void 0);
|
|
@@ -2447,6 +2713,10 @@ var init_Pipeline = __esm({
|
|
|
2447
2713
|
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
2448
2714
|
* making individual getAppliedTags() calls.
|
|
2449
2715
|
*
|
|
2716
|
+
* Uses a persistent tag cache across pipeline runs — tags are static within
|
|
2717
|
+
* a session, so cards seen in a prior run (e.g. before a replan) don't
|
|
2718
|
+
* require a second DB query.
|
|
2719
|
+
*
|
|
2450
2720
|
* @param cards - Cards to hydrate
|
|
2451
2721
|
* @returns Cards with tags populated
|
|
2452
2722
|
*/
|
|
@@ -2454,14 +2724,128 @@ var init_Pipeline = __esm({
|
|
|
2454
2724
|
if (cards.length === 0) {
|
|
2455
2725
|
return cards;
|
|
2456
2726
|
}
|
|
2457
|
-
const
|
|
2458
|
-
const
|
|
2727
|
+
const uncachedIds = [];
|
|
2728
|
+
for (const card of cards) {
|
|
2729
|
+
if (!this._tagCache.has(card.cardId)) {
|
|
2730
|
+
uncachedIds.push(card.cardId);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
if (uncachedIds.length > 0) {
|
|
2734
|
+
const freshTags = await this.course.getAppliedTagsBatch(uncachedIds);
|
|
2735
|
+
for (const [cardId, tags] of freshTags) {
|
|
2736
|
+
this._tagCache.set(cardId, tags);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
2740
|
+
for (const card of cards) {
|
|
2741
|
+
tagsByCard.set(card.cardId, this._tagCache.get(card.cardId) ?? []);
|
|
2742
|
+
}
|
|
2459
2743
|
logTagHydration(cards, tagsByCard);
|
|
2460
2744
|
return cards.map((card) => ({
|
|
2461
2745
|
...card,
|
|
2462
|
-
tags:
|
|
2746
|
+
tags: this._tagCache.get(card.cardId) ?? []
|
|
2463
2747
|
}));
|
|
2464
2748
|
}
|
|
2749
|
+
// ---------------------------------------------------------------------------
|
|
2750
|
+
// Ephemeral hints application
|
|
2751
|
+
// ---------------------------------------------------------------------------
|
|
2752
|
+
/**
|
|
2753
|
+
* Apply one-shot replan hints to the post-filter card set.
|
|
2754
|
+
*
|
|
2755
|
+
* Order of operations:
|
|
2756
|
+
* 1. Exclude (remove unwanted cards)
|
|
2757
|
+
* 2. Boost (multiply scores)
|
|
2758
|
+
* 3. Require (inject must-have cards from the full pre-filter pool)
|
|
2759
|
+
*
|
|
2760
|
+
* @param cards - Post-filter cards (score > 0)
|
|
2761
|
+
* @param hints - The ephemeral hints to apply
|
|
2762
|
+
* @param allCards - Full pre-filter card pool (for require injection)
|
|
2763
|
+
*/
|
|
2764
|
+
applyHints(cards, hints, allCards) {
|
|
2765
|
+
const beforeCount = cards.length;
|
|
2766
|
+
if (hints.excludeCards?.length) {
|
|
2767
|
+
cards = cards.filter(
|
|
2768
|
+
(c) => !hints.excludeCards.some((pat) => globMatch(c.cardId, pat))
|
|
2769
|
+
);
|
|
2770
|
+
}
|
|
2771
|
+
if (hints.excludeTags?.length) {
|
|
2772
|
+
cards = cards.filter(
|
|
2773
|
+
(c) => !hints.excludeTags.some((pat) => cardMatchesTagPattern(c, pat))
|
|
2774
|
+
);
|
|
2775
|
+
}
|
|
2776
|
+
if (hints.boostTags) {
|
|
2777
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags)) {
|
|
2778
|
+
for (const card of cards) {
|
|
2779
|
+
if (cardMatchesTagPattern(card, pattern)) {
|
|
2780
|
+
card.score *= factor;
|
|
2781
|
+
card.provenance.push({
|
|
2782
|
+
strategy: "ephemeralHint",
|
|
2783
|
+
strategyId: "ephemeral-hint",
|
|
2784
|
+
strategyName: "Replan Hint",
|
|
2785
|
+
action: "boosted",
|
|
2786
|
+
score: card.score,
|
|
2787
|
+
reason: `boostTag ${pattern} \xD7${factor}`
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
if (hints.boostCards) {
|
|
2794
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards)) {
|
|
2795
|
+
for (const card of cards) {
|
|
2796
|
+
if (globMatch(card.cardId, pattern)) {
|
|
2797
|
+
card.score *= factor;
|
|
2798
|
+
card.provenance.push({
|
|
2799
|
+
strategy: "ephemeralHint",
|
|
2800
|
+
strategyId: "ephemeral-hint",
|
|
2801
|
+
strategyName: "Replan Hint",
|
|
2802
|
+
action: "boosted",
|
|
2803
|
+
score: card.score,
|
|
2804
|
+
reason: `boostCard ${pattern} \xD7${factor}`
|
|
2805
|
+
});
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
const cardIds = new Set(cards.map((c) => c.cardId));
|
|
2811
|
+
const inject = (card, reason) => {
|
|
2812
|
+
if (!cardIds.has(card.cardId)) {
|
|
2813
|
+
const floorScore = Math.max(card.score, 1);
|
|
2814
|
+
cards.push({
|
|
2815
|
+
...card,
|
|
2816
|
+
score: floorScore,
|
|
2817
|
+
provenance: [
|
|
2818
|
+
...card.provenance,
|
|
2819
|
+
{
|
|
2820
|
+
strategy: "ephemeralHint",
|
|
2821
|
+
strategyId: "ephemeral-hint",
|
|
2822
|
+
strategyName: "Replan Hint",
|
|
2823
|
+
action: "boosted",
|
|
2824
|
+
score: floorScore,
|
|
2825
|
+
reason
|
|
2826
|
+
}
|
|
2827
|
+
]
|
|
2828
|
+
});
|
|
2829
|
+
cardIds.add(card.cardId);
|
|
2830
|
+
}
|
|
2831
|
+
};
|
|
2832
|
+
if (hints.requireCards?.length) {
|
|
2833
|
+
for (const pattern of hints.requireCards) {
|
|
2834
|
+
for (const card of allCards) {
|
|
2835
|
+
if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2839
|
+
if (hints.requireTags?.length) {
|
|
2840
|
+
for (const pattern of hints.requireTags) {
|
|
2841
|
+
for (const card of allCards) {
|
|
2842
|
+
if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
logger.info(`[Pipeline] Hints applied: ${beforeCount} \u2192 ${cards.length} cards`);
|
|
2847
|
+
return cards;
|
|
2848
|
+
}
|
|
2465
2849
|
/**
|
|
2466
2850
|
* Build shared context for generator and filters.
|
|
2467
2851
|
*
|
|
@@ -2479,7 +2863,10 @@ var init_Pipeline = __esm({
|
|
|
2479
2863
|
} catch (e) {
|
|
2480
2864
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
2481
2865
|
}
|
|
2482
|
-
|
|
2866
|
+
if (!this._cachedOrchestration) {
|
|
2867
|
+
this._cachedOrchestration = await createOrchestrationContext(this.user, this.course);
|
|
2868
|
+
}
|
|
2869
|
+
const orchestration = this._cachedOrchestration;
|
|
2483
2870
|
return {
|
|
2484
2871
|
user: this.user,
|
|
2485
2872
|
course: this.course,
|
|
@@ -2523,6 +2910,87 @@ var init_Pipeline = __esm({
|
|
|
2523
2910
|
}
|
|
2524
2911
|
return [...new Set(ids)];
|
|
2525
2912
|
}
|
|
2913
|
+
// ---------------------------------------------------------------------------
|
|
2914
|
+
// Card-space diagnostic
|
|
2915
|
+
// ---------------------------------------------------------------------------
|
|
2916
|
+
/**
|
|
2917
|
+
* Scan every card in the course through the filter chain and report
|
|
2918
|
+
* how many are "well indicated" (score >= threshold) for the current user.
|
|
2919
|
+
*
|
|
2920
|
+
* Also reports how many well-indicated cards the user has NOT yet encountered.
|
|
2921
|
+
*
|
|
2922
|
+
* Exposed via `window.skuilder.pipeline.diagnoseCardSpace()`.
|
|
2923
|
+
*/
|
|
2924
|
+
async diagnoseCardSpace(opts) {
|
|
2925
|
+
const THRESHOLD = opts?.threshold ?? 0.1;
|
|
2926
|
+
const t0 = performance.now();
|
|
2927
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
2928
|
+
let cards = allCardIds.map((cardId) => ({
|
|
2929
|
+
cardId,
|
|
2930
|
+
courseId: this.course.getCourseID(),
|
|
2931
|
+
score: 1,
|
|
2932
|
+
provenance: []
|
|
2933
|
+
}));
|
|
2934
|
+
cards = await this.hydrateTags(cards);
|
|
2935
|
+
const context = await this.buildContext();
|
|
2936
|
+
const filterBreakdown = [];
|
|
2937
|
+
for (const filter of this.filters) {
|
|
2938
|
+
cards = await filter.transform(cards, context);
|
|
2939
|
+
const wi = cards.filter((c) => c.score >= THRESHOLD).length;
|
|
2940
|
+
filterBreakdown.push({ name: filter.name, wellIndicated: wi });
|
|
2941
|
+
}
|
|
2942
|
+
const wellIndicated = cards.filter((c) => c.score >= THRESHOLD);
|
|
2943
|
+
const wellIndicatedIds = new Set(wellIndicated.map((c) => c.cardId));
|
|
2944
|
+
let encounteredIds;
|
|
2945
|
+
try {
|
|
2946
|
+
const courseId = this.course.getCourseID();
|
|
2947
|
+
const seenCards = await this.user.getSeenCards(courseId);
|
|
2948
|
+
encounteredIds = new Set(seenCards);
|
|
2949
|
+
} catch {
|
|
2950
|
+
encounteredIds = /* @__PURE__ */ new Set();
|
|
2951
|
+
}
|
|
2952
|
+
const wellIndicatedNew = wellIndicated.filter((c) => !encounteredIds.has(c.cardId));
|
|
2953
|
+
const byType = /* @__PURE__ */ new Map();
|
|
2954
|
+
for (const card of cards) {
|
|
2955
|
+
const type = card.cardId.split("-")[1] || "unknown";
|
|
2956
|
+
if (!byType.has(type)) {
|
|
2957
|
+
byType.set(type, { total: 0, wellIndicated: 0, new: 0 });
|
|
2958
|
+
}
|
|
2959
|
+
const entry = byType.get(type);
|
|
2960
|
+
entry.total++;
|
|
2961
|
+
if (card.score >= THRESHOLD) {
|
|
2962
|
+
entry.wellIndicated++;
|
|
2963
|
+
if (!encounteredIds.has(card.cardId)) entry.new++;
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
const elapsed = performance.now() - t0;
|
|
2967
|
+
const result = {
|
|
2968
|
+
totalCards: allCardIds.length,
|
|
2969
|
+
threshold: THRESHOLD,
|
|
2970
|
+
wellIndicated: wellIndicatedIds.size,
|
|
2971
|
+
encountered: encounteredIds.size,
|
|
2972
|
+
wellIndicatedNew: wellIndicatedNew.length,
|
|
2973
|
+
byType: Object.fromEntries(byType),
|
|
2974
|
+
filterBreakdown,
|
|
2975
|
+
elapsedMs: Math.round(elapsed)
|
|
2976
|
+
};
|
|
2977
|
+
logger.info(`[Pipeline:diagnose] Card space scan (${result.elapsedMs}ms):`);
|
|
2978
|
+
logger.info(`[Pipeline:diagnose] Total cards: ${result.totalCards}`);
|
|
2979
|
+
logger.info(`[Pipeline:diagnose] Well-indicated (score >= ${THRESHOLD}): ${result.wellIndicated}`);
|
|
2980
|
+
logger.info(`[Pipeline:diagnose] Encountered: ${result.encountered}`);
|
|
2981
|
+
logger.info(`[Pipeline:diagnose] Well-indicated & new: ${result.wellIndicatedNew}`);
|
|
2982
|
+
logger.info(`[Pipeline:diagnose] By type:`);
|
|
2983
|
+
for (const [type, counts] of byType) {
|
|
2984
|
+
logger.info(
|
|
2985
|
+
`[Pipeline:diagnose] ${type}: ${counts.wellIndicated}/${counts.total} well-indicated, ${counts.new} new`
|
|
2986
|
+
);
|
|
2987
|
+
}
|
|
2988
|
+
logger.info(`[Pipeline:diagnose] After each filter:`);
|
|
2989
|
+
for (const fb of filterBreakdown) {
|
|
2990
|
+
logger.info(`[Pipeline:diagnose] ${fb.name}: ${fb.wellIndicated} well-indicated`);
|
|
2991
|
+
}
|
|
2992
|
+
return result;
|
|
2993
|
+
}
|
|
2526
2994
|
};
|
|
2527
2995
|
}
|
|
2528
2996
|
});
|
|
@@ -2627,23 +3095,25 @@ var init_PipelineAssembler = __esm({
|
|
|
2627
3095
|
warnings.push(`Unknown strategy type '${s.implementingClass}', skipping: ${s.name}`);
|
|
2628
3096
|
}
|
|
2629
3097
|
}
|
|
3098
|
+
const courseId = course.getCourseID();
|
|
3099
|
+
const hasElo = generatorStrategies.some((s) => s.implementingClass === "elo" /* ELO */);
|
|
3100
|
+
const hasSrs = generatorStrategies.some((s) => s.implementingClass === "srs" /* SRS */);
|
|
3101
|
+
if (!hasElo) {
|
|
3102
|
+
logger.debug("[PipelineAssembler] No ELO generator configured, adding default");
|
|
3103
|
+
generatorStrategies.push(createDefaultEloStrategy(courseId));
|
|
3104
|
+
}
|
|
3105
|
+
if (!hasSrs) {
|
|
3106
|
+
logger.debug("[PipelineAssembler] No SRS generator configured, adding default");
|
|
3107
|
+
generatorStrategies.push(createDefaultSrsStrategy(courseId));
|
|
3108
|
+
}
|
|
2630
3109
|
if (generatorStrategies.length === 0) {
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
} else {
|
|
2639
|
-
warnings.push("No generator strategy found");
|
|
2640
|
-
return {
|
|
2641
|
-
pipeline: null,
|
|
2642
|
-
generatorStrategies: [],
|
|
2643
|
-
filterStrategies: [],
|
|
2644
|
-
warnings
|
|
2645
|
-
};
|
|
2646
|
-
}
|
|
3110
|
+
warnings.push("No generator strategy found");
|
|
3111
|
+
return {
|
|
3112
|
+
pipeline: null,
|
|
3113
|
+
generatorStrategies: [],
|
|
3114
|
+
filterStrategies: [],
|
|
3115
|
+
warnings
|
|
3116
|
+
};
|
|
2647
3117
|
}
|
|
2648
3118
|
let generator;
|
|
2649
3119
|
if (generatorStrategies.length === 1) {
|
|
@@ -2721,6 +3191,7 @@ var init_3 = __esm({
|
|
|
2721
3191
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
2722
3192
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
2723
3193
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
3194
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
2724
3195
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
2725
3196
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
2726
3197
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
@@ -2738,6 +3209,7 @@ __export(navigators_exports, {
|
|
|
2738
3209
|
getCardOrigin: () => getCardOrigin,
|
|
2739
3210
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
2740
3211
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
3212
|
+
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
2741
3213
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
2742
3214
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
2743
3215
|
isFilter: () => isFilter,
|
|
@@ -2746,16 +3218,19 @@ __export(navigators_exports, {
|
|
|
2746
3218
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
2747
3219
|
registerNavigator: () => registerNavigator
|
|
2748
3220
|
});
|
|
2749
|
-
function registerNavigator(implementingClass, constructor) {
|
|
2750
|
-
navigatorRegistry.set(implementingClass, constructor);
|
|
2751
|
-
logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}`);
|
|
3221
|
+
function registerNavigator(implementingClass, constructor, role) {
|
|
3222
|
+
navigatorRegistry.set(implementingClass, { constructor, role });
|
|
3223
|
+
logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}${role ? ` (${role})` : ""}`);
|
|
2752
3224
|
}
|
|
2753
3225
|
function getRegisteredNavigator(implementingClass) {
|
|
2754
|
-
return navigatorRegistry.get(implementingClass);
|
|
3226
|
+
return navigatorRegistry.get(implementingClass)?.constructor;
|
|
2755
3227
|
}
|
|
2756
3228
|
function hasRegisteredNavigator(implementingClass) {
|
|
2757
3229
|
return navigatorRegistry.has(implementingClass);
|
|
2758
3230
|
}
|
|
3231
|
+
function getRegisteredNavigatorRole(implementingClass) {
|
|
3232
|
+
return navigatorRegistry.get(implementingClass)?.role;
|
|
3233
|
+
}
|
|
2759
3234
|
function getRegisteredNavigatorNames() {
|
|
2760
3235
|
return Array.from(navigatorRegistry.keys());
|
|
2761
3236
|
}
|
|
@@ -2765,8 +3240,10 @@ async function initializeNavigatorRegistry() {
|
|
|
2765
3240
|
Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
2766
3241
|
Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
2767
3242
|
]);
|
|
3243
|
+
const prescribedModule = await Promise.resolve().then(() => (init_prescribed(), prescribed_exports));
|
|
2768
3244
|
registerNavigator("elo", eloModule.default);
|
|
2769
3245
|
registerNavigator("srs", srsModule.default);
|
|
3246
|
+
registerNavigator("prescribed", prescribedModule.default);
|
|
2770
3247
|
const [
|
|
2771
3248
|
hierarchyModule,
|
|
2772
3249
|
interferenceModule,
|
|
@@ -2801,10 +3278,12 @@ function getCardOrigin(card) {
|
|
|
2801
3278
|
return "new";
|
|
2802
3279
|
}
|
|
2803
3280
|
function isGenerator(impl) {
|
|
2804
|
-
|
|
3281
|
+
if (NavigatorRoles[impl] === "generator" /* GENERATOR */) return true;
|
|
3282
|
+
return getRegisteredNavigatorRole(impl) === "generator" /* GENERATOR */;
|
|
2805
3283
|
}
|
|
2806
3284
|
function isFilter(impl) {
|
|
2807
|
-
|
|
3285
|
+
if (NavigatorRoles[impl] === "filter" /* FILTER */) return true;
|
|
3286
|
+
return getRegisteredNavigatorRole(impl) === "filter" /* FILTER */;
|
|
2808
3287
|
}
|
|
2809
3288
|
var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigator;
|
|
2810
3289
|
var init_navigators = __esm({
|
|
@@ -2819,6 +3298,7 @@ var init_navigators = __esm({
|
|
|
2819
3298
|
Navigators = /* @__PURE__ */ ((Navigators2) => {
|
|
2820
3299
|
Navigators2["ELO"] = "elo";
|
|
2821
3300
|
Navigators2["SRS"] = "srs";
|
|
3301
|
+
Navigators2["PRESCRIBED"] = "prescribed";
|
|
2822
3302
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
2823
3303
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
2824
3304
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
@@ -2833,6 +3313,7 @@ var init_navigators = __esm({
|
|
|
2833
3313
|
NavigatorRoles = {
|
|
2834
3314
|
["elo" /* ELO */]: "generator" /* GENERATOR */,
|
|
2835
3315
|
["srs" /* SRS */]: "generator" /* GENERATOR */,
|
|
3316
|
+
["prescribed" /* PRESCRIBED */]: "generator" /* GENERATOR */,
|
|
2836
3317
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
2837
3318
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
2838
3319
|
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
@@ -2997,6 +3478,12 @@ var init_navigators = __esm({
|
|
|
2997
3478
|
async getWeightedCards(_limit) {
|
|
2998
3479
|
throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
|
|
2999
3480
|
}
|
|
3481
|
+
/**
|
|
3482
|
+
* Set ephemeral hints for the next pipeline run.
|
|
3483
|
+
* No-op for non-Pipeline navigators. Pipeline overrides this.
|
|
3484
|
+
*/
|
|
3485
|
+
setEphemeralHints(_hints) {
|
|
3486
|
+
}
|
|
3000
3487
|
};
|
|
3001
3488
|
}
|
|
3002
3489
|
});
|
|
@@ -3047,6 +3534,16 @@ var init_adminDB2 = __esm({
|
|
|
3047
3534
|
}
|
|
3048
3535
|
});
|
|
3049
3536
|
|
|
3537
|
+
// src/impl/couch/CourseSyncService.ts
|
|
3538
|
+
var init_CourseSyncService = __esm({
|
|
3539
|
+
"src/impl/couch/CourseSyncService.ts"() {
|
|
3540
|
+
"use strict";
|
|
3541
|
+
init_pouchdb_setup();
|
|
3542
|
+
init_couch();
|
|
3543
|
+
init_logger();
|
|
3544
|
+
}
|
|
3545
|
+
});
|
|
3546
|
+
|
|
3050
3547
|
// src/impl/couch/auth.ts
|
|
3051
3548
|
var import_cross_fetch;
|
|
3052
3549
|
var init_auth = __esm({
|
|
@@ -3110,6 +3607,7 @@ var init_couch = __esm({
|
|
|
3110
3607
|
init_classroomDB2();
|
|
3111
3608
|
init_courseAPI();
|
|
3112
3609
|
init_courseDB();
|
|
3610
|
+
init_CourseSyncService();
|
|
3113
3611
|
init_CouchDBSyncStrategy();
|
|
3114
3612
|
isBrowser = typeof window !== "undefined";
|
|
3115
3613
|
if (isBrowser) {
|
|
@@ -3409,6 +3907,9 @@ Currently logged-in as ${this._username}.`
|
|
|
3409
3907
|
const id = row.id;
|
|
3410
3908
|
return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
|
|
3411
3909
|
id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
|
|
3910
|
+
id.startsWith(DocTypePrefixes["STRATEGY_STATE" /* STRATEGY_STATE */]) || // Strategy state (user prefs, progression)
|
|
3911
|
+
id.startsWith(DocTypePrefixes["USER_OUTCOME" /* USER_OUTCOME */]) || // Evolutionary orchestration outcomes
|
|
3912
|
+
id.startsWith(DocTypePrefixes["STRATEGY_LEARNING_STATE" /* STRATEGY_LEARNING_STATE */]) || // Strategy learning state
|
|
3412
3913
|
id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
3413
3914
|
id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
3414
3915
|
id === _BaseUser.DOC_IDS.CONFIG;
|
|
@@ -5264,6 +5765,10 @@ var init_courseDB3 = __esm({
|
|
|
5264
5765
|
}
|
|
5265
5766
|
return tagsByCard;
|
|
5266
5767
|
}
|
|
5768
|
+
async getAllCardIds() {
|
|
5769
|
+
const tagsIndex = await this.unpacker.getTagsIndex();
|
|
5770
|
+
return Object.keys(tagsIndex.byCard);
|
|
5771
|
+
}
|
|
5267
5772
|
async addTagToCard(_cardId, _tagId) {
|
|
5268
5773
|
throw new Error("Cannot modify tags in static mode");
|
|
5269
5774
|
}
|