@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
package/dist/core/index.mjs
CHANGED
|
@@ -702,8 +702,12 @@ __export(PipelineDebugger_exports, {
|
|
|
702
702
|
buildRunReport: () => buildRunReport,
|
|
703
703
|
captureRun: () => captureRun,
|
|
704
704
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
705
|
-
pipelineDebugAPI: () => pipelineDebugAPI
|
|
705
|
+
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
706
|
+
registerPipelineForDebug: () => registerPipelineForDebug
|
|
706
707
|
});
|
|
708
|
+
function registerPipelineForDebug(pipeline) {
|
|
709
|
+
_activePipeline = pipeline;
|
|
710
|
+
}
|
|
707
711
|
function getOrigin(card) {
|
|
708
712
|
const firstEntry = card.provenance[0];
|
|
709
713
|
if (!firstEntry) return "unknown";
|
|
@@ -731,6 +735,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
731
735
|
origin: getOrigin(card),
|
|
732
736
|
finalScore: card.score,
|
|
733
737
|
provenance: card.provenance,
|
|
738
|
+
tags: card.tags,
|
|
734
739
|
selected: selectedIds.has(card.cardId)
|
|
735
740
|
}));
|
|
736
741
|
const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
|
|
@@ -786,11 +791,13 @@ function mountPipelineDebugger() {
|
|
|
786
791
|
win.skuilder = win.skuilder || {};
|
|
787
792
|
win.skuilder.pipeline = pipelineDebugAPI;
|
|
788
793
|
}
|
|
789
|
-
var MAX_RUNS, runHistory, pipelineDebugAPI;
|
|
794
|
+
var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
|
|
790
795
|
var init_PipelineDebugger = __esm({
|
|
791
796
|
"src/core/navigators/PipelineDebugger.ts"() {
|
|
792
797
|
"use strict";
|
|
798
|
+
init_navigators();
|
|
793
799
|
init_logger();
|
|
800
|
+
_activePipeline = null;
|
|
794
801
|
MAX_RUNS = 10;
|
|
795
802
|
runHistory = [];
|
|
796
803
|
pipelineDebugAPI = {
|
|
@@ -932,6 +939,81 @@ var init_PipelineDebugger = __esm({
|
|
|
932
939
|
runHistory.length = 0;
|
|
933
940
|
logger.info("[Pipeline Debug] Run history cleared.");
|
|
934
941
|
},
|
|
942
|
+
/**
|
|
943
|
+
* Show the navigator registry: all registered classes and their roles.
|
|
944
|
+
*
|
|
945
|
+
* Useful for verifying that consumer-defined navigators were registered
|
|
946
|
+
* before pipeline assembly.
|
|
947
|
+
*/
|
|
948
|
+
showRegistry() {
|
|
949
|
+
const names = getRegisteredNavigatorNames();
|
|
950
|
+
if (names.length === 0) {
|
|
951
|
+
logger.info("[Pipeline Debug] Navigator registry is empty.");
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
console.group("\u{1F4E6} Navigator Registry");
|
|
955
|
+
console.table(
|
|
956
|
+
names.map((name) => {
|
|
957
|
+
const registryRole = getRegisteredNavigatorRole(name);
|
|
958
|
+
const builtinRole = NavigatorRoles[name];
|
|
959
|
+
const effectiveRole = builtinRole || registryRole || "\u26A0\uFE0F NONE";
|
|
960
|
+
const source = builtinRole ? "built-in" : registryRole ? "consumer" : "unclassified";
|
|
961
|
+
return {
|
|
962
|
+
name,
|
|
963
|
+
role: effectiveRole,
|
|
964
|
+
source,
|
|
965
|
+
isGenerator: isGenerator(name),
|
|
966
|
+
isFilter: isFilter(name)
|
|
967
|
+
};
|
|
968
|
+
})
|
|
969
|
+
);
|
|
970
|
+
console.groupEnd();
|
|
971
|
+
},
|
|
972
|
+
/**
|
|
973
|
+
* Show strategy documents from the last pipeline run and how they mapped
|
|
974
|
+
* to the registry.
|
|
975
|
+
*
|
|
976
|
+
* If no runs are captured yet, falls back to showing just the registry.
|
|
977
|
+
*/
|
|
978
|
+
showStrategies() {
|
|
979
|
+
this.showRegistry();
|
|
980
|
+
if (runHistory.length === 0) {
|
|
981
|
+
logger.info("[Pipeline Debug] No pipeline runs captured yet \u2014 cannot show strategy doc mapping.");
|
|
982
|
+
return;
|
|
983
|
+
}
|
|
984
|
+
const run = runHistory[0];
|
|
985
|
+
console.group("\u{1F50C} Pipeline Strategy Mapping (last run)");
|
|
986
|
+
logger.info(`Generator: ${run.generatorName}`);
|
|
987
|
+
if (run.generators && run.generators.length > 0) {
|
|
988
|
+
for (const g of run.generators) {
|
|
989
|
+
logger.info(` \u{1F4E5} ${g.name}: ${g.cardCount} cards (${g.newCount} new, ${g.reviewCount} reviews)`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
if (run.filters.length > 0) {
|
|
993
|
+
logger.info("Filters:");
|
|
994
|
+
for (const f of run.filters) {
|
|
995
|
+
logger.info(` \u{1F538} ${f.name}: \u2191${f.boosted} \u2193${f.penalized} =${f.passed} \u2715${f.removed}`);
|
|
996
|
+
}
|
|
997
|
+
} else {
|
|
998
|
+
logger.info("Filters: (none)");
|
|
999
|
+
}
|
|
1000
|
+
console.groupEnd();
|
|
1001
|
+
},
|
|
1002
|
+
/**
|
|
1003
|
+
* Scan the full card space through the filter chain for the current user.
|
|
1004
|
+
*
|
|
1005
|
+
* Reports how many cards are well-indicated and how many are new.
|
|
1006
|
+
* Use this to understand how the search space grows during onboarding.
|
|
1007
|
+
*
|
|
1008
|
+
* @param threshold - Score threshold for "well indicated" (default 0.10)
|
|
1009
|
+
*/
|
|
1010
|
+
async diagnoseCardSpace(threshold) {
|
|
1011
|
+
if (!_activePipeline) {
|
|
1012
|
+
logger.info("[Pipeline Debug] No active pipeline. Run a session first.");
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
return _activePipeline.diagnoseCardSpace({ threshold });
|
|
1016
|
+
},
|
|
935
1017
|
/**
|
|
936
1018
|
* Show help.
|
|
937
1019
|
*/
|
|
@@ -944,6 +1026,9 @@ Commands:
|
|
|
944
1026
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
945
1027
|
.showCard(cardId) Show provenance trail for a specific card
|
|
946
1028
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
1029
|
+
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
1030
|
+
.showRegistry() Show navigator registry (classes + roles)
|
|
1031
|
+
.showStrategies() Show registry + strategy mapping from last run
|
|
947
1032
|
.listRuns() List all captured runs in table format
|
|
948
1033
|
.export() Export run history as JSON for bug reports
|
|
949
1034
|
.clear() Clear run history
|
|
@@ -953,7 +1038,7 @@ Commands:
|
|
|
953
1038
|
Example:
|
|
954
1039
|
window.skuilder.pipeline.showLastRun()
|
|
955
1040
|
window.skuilder.pipeline.showRun(1)
|
|
956
|
-
window.skuilder.pipeline.
|
|
1041
|
+
await window.skuilder.pipeline.diagnoseCardSpace()
|
|
957
1042
|
`);
|
|
958
1043
|
}
|
|
959
1044
|
};
|
|
@@ -1248,6 +1333,69 @@ var init_generators = __esm({
|
|
|
1248
1333
|
}
|
|
1249
1334
|
});
|
|
1250
1335
|
|
|
1336
|
+
// src/core/navigators/generators/prescribed.ts
|
|
1337
|
+
var prescribed_exports = {};
|
|
1338
|
+
__export(prescribed_exports, {
|
|
1339
|
+
default: () => PrescribedCardsGenerator
|
|
1340
|
+
});
|
|
1341
|
+
var PrescribedCardsGenerator;
|
|
1342
|
+
var init_prescribed = __esm({
|
|
1343
|
+
"src/core/navigators/generators/prescribed.ts"() {
|
|
1344
|
+
"use strict";
|
|
1345
|
+
init_navigators();
|
|
1346
|
+
init_logger();
|
|
1347
|
+
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1348
|
+
name;
|
|
1349
|
+
config;
|
|
1350
|
+
constructor(user, course, strategyData) {
|
|
1351
|
+
super(user, course, strategyData);
|
|
1352
|
+
this.name = strategyData.name || "Prescribed Cards";
|
|
1353
|
+
try {
|
|
1354
|
+
const parsed = JSON.parse(strategyData.serializedData);
|
|
1355
|
+
this.config = { cardIds: parsed.cardIds || [] };
|
|
1356
|
+
} catch {
|
|
1357
|
+
this.config = { cardIds: [] };
|
|
1358
|
+
}
|
|
1359
|
+
logger.debug(
|
|
1360
|
+
`[Prescribed] Initialized with ${this.config.cardIds.length} prescribed cards`
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
async getWeightedCards(limit, _context) {
|
|
1364
|
+
if (this.config.cardIds.length === 0) {
|
|
1365
|
+
return [];
|
|
1366
|
+
}
|
|
1367
|
+
const courseId = this.course.getCourseID();
|
|
1368
|
+
const activeCards = await this.user.getActiveCards();
|
|
1369
|
+
const activeIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
1370
|
+
const eligibleIds = this.config.cardIds.filter((id) => !activeIds.has(id));
|
|
1371
|
+
if (eligibleIds.length === 0) {
|
|
1372
|
+
logger.debug("[Prescribed] All prescribed cards already active, returning empty");
|
|
1373
|
+
return [];
|
|
1374
|
+
}
|
|
1375
|
+
const cards = eligibleIds.slice(0, limit).map((cardId) => ({
|
|
1376
|
+
cardId,
|
|
1377
|
+
courseId,
|
|
1378
|
+
score: 1,
|
|
1379
|
+
provenance: [
|
|
1380
|
+
{
|
|
1381
|
+
strategy: "prescribed",
|
|
1382
|
+
strategyName: this.strategyName || this.name,
|
|
1383
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1384
|
+
action: "generated",
|
|
1385
|
+
score: 1,
|
|
1386
|
+
reason: `Prescribed card (${eligibleIds.length} eligible of ${this.config.cardIds.length} configured)`
|
|
1387
|
+
}
|
|
1388
|
+
]
|
|
1389
|
+
}));
|
|
1390
|
+
logger.info(
|
|
1391
|
+
`[Prescribed] Emitting ${cards.length} cards (${eligibleIds.length} eligible, ${activeIds.size} already active)`
|
|
1392
|
+
);
|
|
1393
|
+
return cards;
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1251
1399
|
// src/core/navigators/generators/srs.ts
|
|
1252
1400
|
var srs_exports = {};
|
|
1253
1401
|
__export(srs_exports, {
|
|
@@ -1442,6 +1590,7 @@ var init_ = __esm({
|
|
|
1442
1590
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
1443
1591
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
1444
1592
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
1593
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
1445
1594
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
1446
1595
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports))
|
|
1447
1596
|
});
|
|
@@ -1642,6 +1791,8 @@ var init_hierarchyDefinition = __esm({
|
|
|
1642
1791
|
if (userTagElo.count < minCount) return false;
|
|
1643
1792
|
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1644
1793
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
1794
|
+
} else if (prereq.masteryThreshold?.minCount !== void 0) {
|
|
1795
|
+
return true;
|
|
1645
1796
|
} else {
|
|
1646
1797
|
return userTagElo.score >= userGlobalElo;
|
|
1647
1798
|
}
|
|
@@ -1717,14 +1868,38 @@ var init_hierarchyDefinition = __esm({
|
|
|
1717
1868
|
};
|
|
1718
1869
|
}
|
|
1719
1870
|
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Build a map of prereq tag → max configured boost for all *closed* gates.
|
|
1873
|
+
*
|
|
1874
|
+
* When a gate is closed (prereqs unmet), cards carrying that gate's prereq
|
|
1875
|
+
* tags get boosted — steering the pipeline toward content that helps unlock
|
|
1876
|
+
* the gated material. Once the gate opens, the boost disappears.
|
|
1877
|
+
*/
|
|
1878
|
+
getPreReqBoosts(unlockedTags, masteredTags) {
|
|
1879
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
1880
|
+
for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
|
|
1881
|
+
if (unlockedTags.has(tagId)) continue;
|
|
1882
|
+
for (const prereq of prereqs) {
|
|
1883
|
+
if (!prereq.preReqBoost || prereq.preReqBoost <= 1) continue;
|
|
1884
|
+
if (masteredTags.has(prereq.tag)) continue;
|
|
1885
|
+
const existing = boosts.get(prereq.tag) ?? 1;
|
|
1886
|
+
boosts.set(prereq.tag, Math.max(existing, prereq.preReqBoost));
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return boosts;
|
|
1890
|
+
}
|
|
1720
1891
|
/**
|
|
1721
1892
|
* CardFilter.transform implementation.
|
|
1722
1893
|
*
|
|
1723
|
-
*
|
|
1894
|
+
* Two effects:
|
|
1895
|
+
* 1. Cards with locked tags receive score * 0.05 (gating penalty)
|
|
1896
|
+
* 2. Cards carrying prereq tags of closed gates receive a configured
|
|
1897
|
+
* boost (preReqBoost), steering toward content that unlocks gates
|
|
1724
1898
|
*/
|
|
1725
1899
|
async transform(cards, context) {
|
|
1726
1900
|
const masteredTags = await this.getMasteredTags(context);
|
|
1727
1901
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
1902
|
+
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
1728
1903
|
const gated = [];
|
|
1729
1904
|
for (const card of cards) {
|
|
1730
1905
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
@@ -1733,9 +1908,27 @@ var init_hierarchyDefinition = __esm({
|
|
|
1733
1908
|
unlockedTags,
|
|
1734
1909
|
masteredTags
|
|
1735
1910
|
);
|
|
1736
|
-
const LOCKED_PENALTY = 0.
|
|
1737
|
-
|
|
1738
|
-
|
|
1911
|
+
const LOCKED_PENALTY = 0.02;
|
|
1912
|
+
let finalScore = isUnlocked ? card.score : card.score * LOCKED_PENALTY;
|
|
1913
|
+
let action = isUnlocked ? "passed" : "penalized";
|
|
1914
|
+
let finalReason = reason;
|
|
1915
|
+
if (isUnlocked && preReqBoosts.size > 0) {
|
|
1916
|
+
const cardTags = card.tags ?? [];
|
|
1917
|
+
let maxBoost = 1;
|
|
1918
|
+
const boostedPrereqs = [];
|
|
1919
|
+
for (const tag of cardTags) {
|
|
1920
|
+
const boost = preReqBoosts.get(tag);
|
|
1921
|
+
if (boost && boost > maxBoost) {
|
|
1922
|
+
maxBoost = boost;
|
|
1923
|
+
boostedPrereqs.push(tag);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
if (maxBoost > 1) {
|
|
1927
|
+
finalScore *= maxBoost;
|
|
1928
|
+
action = "boosted";
|
|
1929
|
+
finalReason = `${reason} | preReqBoost \xD7${maxBoost.toFixed(2)} for ${boostedPrereqs.join(", ")}`;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1739
1932
|
gated.push({
|
|
1740
1933
|
...card,
|
|
1741
1934
|
score: finalScore,
|
|
@@ -1747,7 +1940,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1747
1940
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-hierarchy",
|
|
1748
1941
|
action,
|
|
1749
1942
|
score: finalScore,
|
|
1750
|
-
reason
|
|
1943
|
+
reason: finalReason
|
|
1751
1944
|
}
|
|
1752
1945
|
]
|
|
1753
1946
|
});
|
|
@@ -2682,6 +2875,18 @@ __export(Pipeline_exports, {
|
|
|
2682
2875
|
Pipeline: () => Pipeline
|
|
2683
2876
|
});
|
|
2684
2877
|
import { toCourseElo as toCourseElo5 } from "@vue-skuilder/common";
|
|
2878
|
+
function globToRegex(pattern) {
|
|
2879
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
2880
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
2881
|
+
return new RegExp(`^${withWildcards}$`);
|
|
2882
|
+
}
|
|
2883
|
+
function globMatch(value, pattern) {
|
|
2884
|
+
if (!pattern.includes("*")) return value === pattern;
|
|
2885
|
+
return globToRegex(pattern).test(value);
|
|
2886
|
+
}
|
|
2887
|
+
function cardMatchesTagPattern(card, pattern) {
|
|
2888
|
+
return (card.tags ?? []).some((tag) => globMatch(tag, pattern));
|
|
2889
|
+
}
|
|
2685
2890
|
function logPipelineConfig(generator, filters) {
|
|
2686
2891
|
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
2687
2892
|
logger.info(
|
|
@@ -2716,6 +2921,21 @@ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCo
|
|
|
2716
2921
|
\u{1F4A1} Inspect: window.skuilder.pipeline`
|
|
2717
2922
|
);
|
|
2718
2923
|
}
|
|
2924
|
+
function logResultCards(cards) {
|
|
2925
|
+
if (!VERBOSE_RESULTS || cards.length === 0) return;
|
|
2926
|
+
logger.info(`[Pipeline] Results (${cards.length} cards):`);
|
|
2927
|
+
for (let i = 0; i < cards.length; i++) {
|
|
2928
|
+
const c = cards[i];
|
|
2929
|
+
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
2930
|
+
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
2931
|
+
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
2932
|
+
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
2933
|
+
}).join(" | ");
|
|
2934
|
+
logger.info(
|
|
2935
|
+
`[Pipeline] ${String(i + 1).padStart(2)}. ${c.score.toFixed(4)} ${c.cardId} [${tags}]${filters ? ` {${filters}}` : ""}`
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2719
2939
|
function logCardProvenance(cards, maxCards = 3) {
|
|
2720
2940
|
const cardsToLog = cards.slice(0, maxCards);
|
|
2721
2941
|
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
@@ -2730,7 +2950,7 @@ function logCardProvenance(cards, maxCards = 3) {
|
|
|
2730
2950
|
}
|
|
2731
2951
|
}
|
|
2732
2952
|
}
|
|
2733
|
-
var Pipeline;
|
|
2953
|
+
var VERBOSE_RESULTS, Pipeline;
|
|
2734
2954
|
var init_Pipeline = __esm({
|
|
2735
2955
|
"src/core/navigators/Pipeline.ts"() {
|
|
2736
2956
|
"use strict";
|
|
@@ -2738,9 +2958,31 @@ var init_Pipeline = __esm({
|
|
|
2738
2958
|
init_logger();
|
|
2739
2959
|
init_orchestration();
|
|
2740
2960
|
init_PipelineDebugger();
|
|
2961
|
+
VERBOSE_RESULTS = true;
|
|
2741
2962
|
Pipeline = class extends ContentNavigator {
|
|
2742
2963
|
generator;
|
|
2743
2964
|
filters;
|
|
2965
|
+
/**
|
|
2966
|
+
* Cached orchestration context. Course config and salt don't change within
|
|
2967
|
+
* a page load, so we build the orchestration context once and reuse it on
|
|
2968
|
+
* subsequent getWeightedCards() calls (e.g. mid-session replans).
|
|
2969
|
+
*
|
|
2970
|
+
* This eliminates a remote getCourseConfig() round trip per pipeline run.
|
|
2971
|
+
*/
|
|
2972
|
+
_cachedOrchestration = null;
|
|
2973
|
+
/**
|
|
2974
|
+
* Persistent tag cache. Maps cardId → tag names.
|
|
2975
|
+
*
|
|
2976
|
+
* Tags are static within a session (they're set at card generation time),
|
|
2977
|
+
* so we cache them across pipeline runs. On replans, many of the same cards
|
|
2978
|
+
* reappear — cache hits avoid redundant remote getAppliedTagsBatch() queries.
|
|
2979
|
+
*/
|
|
2980
|
+
_tagCache = /* @__PURE__ */ new Map();
|
|
2981
|
+
/**
|
|
2982
|
+
* One-shot replan hints. Applied after the filter chain on the next
|
|
2983
|
+
* getWeightedCards() call, then cleared.
|
|
2984
|
+
*/
|
|
2985
|
+
_ephemeralHints = null;
|
|
2744
2986
|
/**
|
|
2745
2987
|
* Create a new pipeline.
|
|
2746
2988
|
*
|
|
@@ -2761,6 +3003,17 @@ var init_Pipeline = __esm({
|
|
|
2761
3003
|
logger.error(`[pipeline] Failed to lookup courseCfg: ${e}`);
|
|
2762
3004
|
});
|
|
2763
3005
|
logPipelineConfig(generator, filters);
|
|
3006
|
+
registerPipelineForDebug(this);
|
|
3007
|
+
}
|
|
3008
|
+
/**
|
|
3009
|
+
* Set one-shot hints for the next pipeline run.
|
|
3010
|
+
* Consumed after one getWeightedCards() call, then cleared.
|
|
3011
|
+
*
|
|
3012
|
+
* Overrides ContentNavigator.setEphemeralHints() no-op.
|
|
3013
|
+
*/
|
|
3014
|
+
setEphemeralHints(hints) {
|
|
3015
|
+
this._ephemeralHints = hints;
|
|
3016
|
+
logger.info(`[Pipeline] Ephemeral hints set: ${JSON.stringify(hints)}`);
|
|
2764
3017
|
}
|
|
2765
3018
|
/**
|
|
2766
3019
|
* Get weighted cards by running generator and applying filters.
|
|
@@ -2777,13 +3030,15 @@ var init_Pipeline = __esm({
|
|
|
2777
3030
|
* @returns Cards sorted by score descending
|
|
2778
3031
|
*/
|
|
2779
3032
|
async getWeightedCards(limit) {
|
|
3033
|
+
const t0 = performance.now();
|
|
2780
3034
|
const context = await this.buildContext();
|
|
2781
|
-
const
|
|
2782
|
-
const fetchLimit =
|
|
3035
|
+
const tContext = performance.now();
|
|
3036
|
+
const fetchLimit = 500;
|
|
2783
3037
|
logger.debug(
|
|
2784
3038
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
2785
3039
|
);
|
|
2786
3040
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
3041
|
+
const tGenerate = performance.now();
|
|
2787
3042
|
const generatedCount = cards.length;
|
|
2788
3043
|
let generatorSummaries;
|
|
2789
3044
|
if (this.generator.generators) {
|
|
@@ -2812,6 +3067,7 @@ var init_Pipeline = __esm({
|
|
|
2812
3067
|
}
|
|
2813
3068
|
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
2814
3069
|
cards = await this.hydrateTags(cards);
|
|
3070
|
+
const tHydrate = performance.now();
|
|
2815
3071
|
const allCardsBeforeFiltering = [...cards];
|
|
2816
3072
|
const filterImpacts = [];
|
|
2817
3073
|
for (const filter of this.filters) {
|
|
@@ -2830,8 +3086,17 @@ var init_Pipeline = __esm({
|
|
|
2830
3086
|
logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
|
|
2831
3087
|
}
|
|
2832
3088
|
cards = cards.filter((c) => c.score > 0);
|
|
3089
|
+
const hints = this._ephemeralHints;
|
|
3090
|
+
if (hints) {
|
|
3091
|
+
this._ephemeralHints = null;
|
|
3092
|
+
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
3093
|
+
}
|
|
2833
3094
|
cards.sort((a, b) => b.score - a.score);
|
|
3095
|
+
const tFilter = performance.now();
|
|
2834
3096
|
const result = cards.slice(0, limit);
|
|
3097
|
+
logger.info(
|
|
3098
|
+
`[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)})`
|
|
3099
|
+
);
|
|
2835
3100
|
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
2836
3101
|
logExecutionSummary(
|
|
2837
3102
|
this.generator.name,
|
|
@@ -2841,6 +3106,7 @@ var init_Pipeline = __esm({
|
|
|
2841
3106
|
topScores,
|
|
2842
3107
|
filterImpacts
|
|
2843
3108
|
);
|
|
3109
|
+
logResultCards(result);
|
|
2844
3110
|
logCardProvenance(result, 3);
|
|
2845
3111
|
try {
|
|
2846
3112
|
const courseName = await this.course?.getCourseConfig().then((c) => c.name).catch(() => void 0);
|
|
@@ -2867,6 +3133,10 @@ var init_Pipeline = __esm({
|
|
|
2867
3133
|
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
2868
3134
|
* making individual getAppliedTags() calls.
|
|
2869
3135
|
*
|
|
3136
|
+
* Uses a persistent tag cache across pipeline runs — tags are static within
|
|
3137
|
+
* a session, so cards seen in a prior run (e.g. before a replan) don't
|
|
3138
|
+
* require a second DB query.
|
|
3139
|
+
*
|
|
2870
3140
|
* @param cards - Cards to hydrate
|
|
2871
3141
|
* @returns Cards with tags populated
|
|
2872
3142
|
*/
|
|
@@ -2874,14 +3144,128 @@ var init_Pipeline = __esm({
|
|
|
2874
3144
|
if (cards.length === 0) {
|
|
2875
3145
|
return cards;
|
|
2876
3146
|
}
|
|
2877
|
-
const
|
|
2878
|
-
const
|
|
3147
|
+
const uncachedIds = [];
|
|
3148
|
+
for (const card of cards) {
|
|
3149
|
+
if (!this._tagCache.has(card.cardId)) {
|
|
3150
|
+
uncachedIds.push(card.cardId);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
if (uncachedIds.length > 0) {
|
|
3154
|
+
const freshTags = await this.course.getAppliedTagsBatch(uncachedIds);
|
|
3155
|
+
for (const [cardId, tags] of freshTags) {
|
|
3156
|
+
this._tagCache.set(cardId, tags);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
3160
|
+
for (const card of cards) {
|
|
3161
|
+
tagsByCard.set(card.cardId, this._tagCache.get(card.cardId) ?? []);
|
|
3162
|
+
}
|
|
2879
3163
|
logTagHydration(cards, tagsByCard);
|
|
2880
3164
|
return cards.map((card) => ({
|
|
2881
3165
|
...card,
|
|
2882
|
-
tags:
|
|
3166
|
+
tags: this._tagCache.get(card.cardId) ?? []
|
|
2883
3167
|
}));
|
|
2884
3168
|
}
|
|
3169
|
+
// ---------------------------------------------------------------------------
|
|
3170
|
+
// Ephemeral hints application
|
|
3171
|
+
// ---------------------------------------------------------------------------
|
|
3172
|
+
/**
|
|
3173
|
+
* Apply one-shot replan hints to the post-filter card set.
|
|
3174
|
+
*
|
|
3175
|
+
* Order of operations:
|
|
3176
|
+
* 1. Exclude (remove unwanted cards)
|
|
3177
|
+
* 2. Boost (multiply scores)
|
|
3178
|
+
* 3. Require (inject must-have cards from the full pre-filter pool)
|
|
3179
|
+
*
|
|
3180
|
+
* @param cards - Post-filter cards (score > 0)
|
|
3181
|
+
* @param hints - The ephemeral hints to apply
|
|
3182
|
+
* @param allCards - Full pre-filter card pool (for require injection)
|
|
3183
|
+
*/
|
|
3184
|
+
applyHints(cards, hints, allCards) {
|
|
3185
|
+
const beforeCount = cards.length;
|
|
3186
|
+
if (hints.excludeCards?.length) {
|
|
3187
|
+
cards = cards.filter(
|
|
3188
|
+
(c) => !hints.excludeCards.some((pat) => globMatch(c.cardId, pat))
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
3191
|
+
if (hints.excludeTags?.length) {
|
|
3192
|
+
cards = cards.filter(
|
|
3193
|
+
(c) => !hints.excludeTags.some((pat) => cardMatchesTagPattern(c, pat))
|
|
3194
|
+
);
|
|
3195
|
+
}
|
|
3196
|
+
if (hints.boostTags) {
|
|
3197
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags)) {
|
|
3198
|
+
for (const card of cards) {
|
|
3199
|
+
if (cardMatchesTagPattern(card, pattern)) {
|
|
3200
|
+
card.score *= factor;
|
|
3201
|
+
card.provenance.push({
|
|
3202
|
+
strategy: "ephemeralHint",
|
|
3203
|
+
strategyId: "ephemeral-hint",
|
|
3204
|
+
strategyName: "Replan Hint",
|
|
3205
|
+
action: "boosted",
|
|
3206
|
+
score: card.score,
|
|
3207
|
+
reason: `boostTag ${pattern} \xD7${factor}`
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
if (hints.boostCards) {
|
|
3214
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards)) {
|
|
3215
|
+
for (const card of cards) {
|
|
3216
|
+
if (globMatch(card.cardId, pattern)) {
|
|
3217
|
+
card.score *= factor;
|
|
3218
|
+
card.provenance.push({
|
|
3219
|
+
strategy: "ephemeralHint",
|
|
3220
|
+
strategyId: "ephemeral-hint",
|
|
3221
|
+
strategyName: "Replan Hint",
|
|
3222
|
+
action: "boosted",
|
|
3223
|
+
score: card.score,
|
|
3224
|
+
reason: `boostCard ${pattern} \xD7${factor}`
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
const cardIds = new Set(cards.map((c) => c.cardId));
|
|
3231
|
+
const inject = (card, reason) => {
|
|
3232
|
+
if (!cardIds.has(card.cardId)) {
|
|
3233
|
+
const floorScore = Math.max(card.score, 1);
|
|
3234
|
+
cards.push({
|
|
3235
|
+
...card,
|
|
3236
|
+
score: floorScore,
|
|
3237
|
+
provenance: [
|
|
3238
|
+
...card.provenance,
|
|
3239
|
+
{
|
|
3240
|
+
strategy: "ephemeralHint",
|
|
3241
|
+
strategyId: "ephemeral-hint",
|
|
3242
|
+
strategyName: "Replan Hint",
|
|
3243
|
+
action: "boosted",
|
|
3244
|
+
score: floorScore,
|
|
3245
|
+
reason
|
|
3246
|
+
}
|
|
3247
|
+
]
|
|
3248
|
+
});
|
|
3249
|
+
cardIds.add(card.cardId);
|
|
3250
|
+
}
|
|
3251
|
+
};
|
|
3252
|
+
if (hints.requireCards?.length) {
|
|
3253
|
+
for (const pattern of hints.requireCards) {
|
|
3254
|
+
for (const card of allCards) {
|
|
3255
|
+
if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
if (hints.requireTags?.length) {
|
|
3260
|
+
for (const pattern of hints.requireTags) {
|
|
3261
|
+
for (const card of allCards) {
|
|
3262
|
+
if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
logger.info(`[Pipeline] Hints applied: ${beforeCount} \u2192 ${cards.length} cards`);
|
|
3267
|
+
return cards;
|
|
3268
|
+
}
|
|
2885
3269
|
/**
|
|
2886
3270
|
* Build shared context for generator and filters.
|
|
2887
3271
|
*
|
|
@@ -2899,7 +3283,10 @@ var init_Pipeline = __esm({
|
|
|
2899
3283
|
} catch (e) {
|
|
2900
3284
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
2901
3285
|
}
|
|
2902
|
-
|
|
3286
|
+
if (!this._cachedOrchestration) {
|
|
3287
|
+
this._cachedOrchestration = await createOrchestrationContext(this.user, this.course);
|
|
3288
|
+
}
|
|
3289
|
+
const orchestration = this._cachedOrchestration;
|
|
2903
3290
|
return {
|
|
2904
3291
|
user: this.user,
|
|
2905
3292
|
course: this.course,
|
|
@@ -2943,6 +3330,87 @@ var init_Pipeline = __esm({
|
|
|
2943
3330
|
}
|
|
2944
3331
|
return [...new Set(ids)];
|
|
2945
3332
|
}
|
|
3333
|
+
// ---------------------------------------------------------------------------
|
|
3334
|
+
// Card-space diagnostic
|
|
3335
|
+
// ---------------------------------------------------------------------------
|
|
3336
|
+
/**
|
|
3337
|
+
* Scan every card in the course through the filter chain and report
|
|
3338
|
+
* how many are "well indicated" (score >= threshold) for the current user.
|
|
3339
|
+
*
|
|
3340
|
+
* Also reports how many well-indicated cards the user has NOT yet encountered.
|
|
3341
|
+
*
|
|
3342
|
+
* Exposed via `window.skuilder.pipeline.diagnoseCardSpace()`.
|
|
3343
|
+
*/
|
|
3344
|
+
async diagnoseCardSpace(opts) {
|
|
3345
|
+
const THRESHOLD = opts?.threshold ?? 0.1;
|
|
3346
|
+
const t0 = performance.now();
|
|
3347
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
3348
|
+
let cards = allCardIds.map((cardId) => ({
|
|
3349
|
+
cardId,
|
|
3350
|
+
courseId: this.course.getCourseID(),
|
|
3351
|
+
score: 1,
|
|
3352
|
+
provenance: []
|
|
3353
|
+
}));
|
|
3354
|
+
cards = await this.hydrateTags(cards);
|
|
3355
|
+
const context = await this.buildContext();
|
|
3356
|
+
const filterBreakdown = [];
|
|
3357
|
+
for (const filter of this.filters) {
|
|
3358
|
+
cards = await filter.transform(cards, context);
|
|
3359
|
+
const wi = cards.filter((c) => c.score >= THRESHOLD).length;
|
|
3360
|
+
filterBreakdown.push({ name: filter.name, wellIndicated: wi });
|
|
3361
|
+
}
|
|
3362
|
+
const wellIndicated = cards.filter((c) => c.score >= THRESHOLD);
|
|
3363
|
+
const wellIndicatedIds = new Set(wellIndicated.map((c) => c.cardId));
|
|
3364
|
+
let encounteredIds;
|
|
3365
|
+
try {
|
|
3366
|
+
const courseId = this.course.getCourseID();
|
|
3367
|
+
const seenCards = await this.user.getSeenCards(courseId);
|
|
3368
|
+
encounteredIds = new Set(seenCards);
|
|
3369
|
+
} catch {
|
|
3370
|
+
encounteredIds = /* @__PURE__ */ new Set();
|
|
3371
|
+
}
|
|
3372
|
+
const wellIndicatedNew = wellIndicated.filter((c) => !encounteredIds.has(c.cardId));
|
|
3373
|
+
const byType = /* @__PURE__ */ new Map();
|
|
3374
|
+
for (const card of cards) {
|
|
3375
|
+
const type = card.cardId.split("-")[1] || "unknown";
|
|
3376
|
+
if (!byType.has(type)) {
|
|
3377
|
+
byType.set(type, { total: 0, wellIndicated: 0, new: 0 });
|
|
3378
|
+
}
|
|
3379
|
+
const entry = byType.get(type);
|
|
3380
|
+
entry.total++;
|
|
3381
|
+
if (card.score >= THRESHOLD) {
|
|
3382
|
+
entry.wellIndicated++;
|
|
3383
|
+
if (!encounteredIds.has(card.cardId)) entry.new++;
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
const elapsed = performance.now() - t0;
|
|
3387
|
+
const result = {
|
|
3388
|
+
totalCards: allCardIds.length,
|
|
3389
|
+
threshold: THRESHOLD,
|
|
3390
|
+
wellIndicated: wellIndicatedIds.size,
|
|
3391
|
+
encountered: encounteredIds.size,
|
|
3392
|
+
wellIndicatedNew: wellIndicatedNew.length,
|
|
3393
|
+
byType: Object.fromEntries(byType),
|
|
3394
|
+
filterBreakdown,
|
|
3395
|
+
elapsedMs: Math.round(elapsed)
|
|
3396
|
+
};
|
|
3397
|
+
logger.info(`[Pipeline:diagnose] Card space scan (${result.elapsedMs}ms):`);
|
|
3398
|
+
logger.info(`[Pipeline:diagnose] Total cards: ${result.totalCards}`);
|
|
3399
|
+
logger.info(`[Pipeline:diagnose] Well-indicated (score >= ${THRESHOLD}): ${result.wellIndicated}`);
|
|
3400
|
+
logger.info(`[Pipeline:diagnose] Encountered: ${result.encountered}`);
|
|
3401
|
+
logger.info(`[Pipeline:diagnose] Well-indicated & new: ${result.wellIndicatedNew}`);
|
|
3402
|
+
logger.info(`[Pipeline:diagnose] By type:`);
|
|
3403
|
+
for (const [type, counts] of byType) {
|
|
3404
|
+
logger.info(
|
|
3405
|
+
`[Pipeline:diagnose] ${type}: ${counts.wellIndicated}/${counts.total} well-indicated, ${counts.new} new`
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
logger.info(`[Pipeline:diagnose] After each filter:`);
|
|
3409
|
+
for (const fb of filterBreakdown) {
|
|
3410
|
+
logger.info(`[Pipeline:diagnose] ${fb.name}: ${fb.wellIndicated} well-indicated`);
|
|
3411
|
+
}
|
|
3412
|
+
return result;
|
|
3413
|
+
}
|
|
2946
3414
|
};
|
|
2947
3415
|
}
|
|
2948
3416
|
});
|
|
@@ -3047,23 +3515,25 @@ var init_PipelineAssembler = __esm({
|
|
|
3047
3515
|
warnings.push(`Unknown strategy type '${s.implementingClass}', skipping: ${s.name}`);
|
|
3048
3516
|
}
|
|
3049
3517
|
}
|
|
3518
|
+
const courseId = course.getCourseID();
|
|
3519
|
+
const hasElo = generatorStrategies.some((s) => s.implementingClass === "elo" /* ELO */);
|
|
3520
|
+
const hasSrs = generatorStrategies.some((s) => s.implementingClass === "srs" /* SRS */);
|
|
3521
|
+
if (!hasElo) {
|
|
3522
|
+
logger.debug("[PipelineAssembler] No ELO generator configured, adding default");
|
|
3523
|
+
generatorStrategies.push(createDefaultEloStrategy(courseId));
|
|
3524
|
+
}
|
|
3525
|
+
if (!hasSrs) {
|
|
3526
|
+
logger.debug("[PipelineAssembler] No SRS generator configured, adding default");
|
|
3527
|
+
generatorStrategies.push(createDefaultSrsStrategy(courseId));
|
|
3528
|
+
}
|
|
3050
3529
|
if (generatorStrategies.length === 0) {
|
|
3051
|
-
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
} else {
|
|
3059
|
-
warnings.push("No generator strategy found");
|
|
3060
|
-
return {
|
|
3061
|
-
pipeline: null,
|
|
3062
|
-
generatorStrategies: [],
|
|
3063
|
-
filterStrategies: [],
|
|
3064
|
-
warnings
|
|
3065
|
-
};
|
|
3066
|
-
}
|
|
3530
|
+
warnings.push("No generator strategy found");
|
|
3531
|
+
return {
|
|
3532
|
+
pipeline: null,
|
|
3533
|
+
generatorStrategies: [],
|
|
3534
|
+
filterStrategies: [],
|
|
3535
|
+
warnings
|
|
3536
|
+
};
|
|
3067
3537
|
}
|
|
3068
3538
|
let generator;
|
|
3069
3539
|
if (generatorStrategies.length === 1) {
|
|
@@ -3141,6 +3611,7 @@ var init_3 = __esm({
|
|
|
3141
3611
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
3142
3612
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
3143
3613
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
3614
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
3144
3615
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
3145
3616
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
3146
3617
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
@@ -3158,6 +3629,7 @@ __export(navigators_exports, {
|
|
|
3158
3629
|
getCardOrigin: () => getCardOrigin,
|
|
3159
3630
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
3160
3631
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
3632
|
+
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
3161
3633
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
3162
3634
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
3163
3635
|
isFilter: () => isFilter,
|
|
@@ -3166,16 +3638,19 @@ __export(navigators_exports, {
|
|
|
3166
3638
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
3167
3639
|
registerNavigator: () => registerNavigator
|
|
3168
3640
|
});
|
|
3169
|
-
function registerNavigator(implementingClass, constructor) {
|
|
3170
|
-
navigatorRegistry.set(implementingClass, constructor);
|
|
3171
|
-
logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}`);
|
|
3641
|
+
function registerNavigator(implementingClass, constructor, role) {
|
|
3642
|
+
navigatorRegistry.set(implementingClass, { constructor, role });
|
|
3643
|
+
logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}${role ? ` (${role})` : ""}`);
|
|
3172
3644
|
}
|
|
3173
3645
|
function getRegisteredNavigator(implementingClass) {
|
|
3174
|
-
return navigatorRegistry.get(implementingClass);
|
|
3646
|
+
return navigatorRegistry.get(implementingClass)?.constructor;
|
|
3175
3647
|
}
|
|
3176
3648
|
function hasRegisteredNavigator(implementingClass) {
|
|
3177
3649
|
return navigatorRegistry.has(implementingClass);
|
|
3178
3650
|
}
|
|
3651
|
+
function getRegisteredNavigatorRole(implementingClass) {
|
|
3652
|
+
return navigatorRegistry.get(implementingClass)?.role;
|
|
3653
|
+
}
|
|
3179
3654
|
function getRegisteredNavigatorNames() {
|
|
3180
3655
|
return Array.from(navigatorRegistry.keys());
|
|
3181
3656
|
}
|
|
@@ -3185,8 +3660,10 @@ async function initializeNavigatorRegistry() {
|
|
|
3185
3660
|
Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
3186
3661
|
Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
3187
3662
|
]);
|
|
3663
|
+
const prescribedModule = await Promise.resolve().then(() => (init_prescribed(), prescribed_exports));
|
|
3188
3664
|
registerNavigator("elo", eloModule.default);
|
|
3189
3665
|
registerNavigator("srs", srsModule.default);
|
|
3666
|
+
registerNavigator("prescribed", prescribedModule.default);
|
|
3190
3667
|
const [
|
|
3191
3668
|
hierarchyModule,
|
|
3192
3669
|
interferenceModule,
|
|
@@ -3221,10 +3698,12 @@ function getCardOrigin(card) {
|
|
|
3221
3698
|
return "new";
|
|
3222
3699
|
}
|
|
3223
3700
|
function isGenerator(impl) {
|
|
3224
|
-
|
|
3701
|
+
if (NavigatorRoles[impl] === "generator" /* GENERATOR */) return true;
|
|
3702
|
+
return getRegisteredNavigatorRole(impl) === "generator" /* GENERATOR */;
|
|
3225
3703
|
}
|
|
3226
3704
|
function isFilter(impl) {
|
|
3227
|
-
|
|
3705
|
+
if (NavigatorRoles[impl] === "filter" /* FILTER */) return true;
|
|
3706
|
+
return getRegisteredNavigatorRole(impl) === "filter" /* FILTER */;
|
|
3228
3707
|
}
|
|
3229
3708
|
var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigator;
|
|
3230
3709
|
var init_navigators = __esm({
|
|
@@ -3239,6 +3718,7 @@ var init_navigators = __esm({
|
|
|
3239
3718
|
Navigators = /* @__PURE__ */ ((Navigators2) => {
|
|
3240
3719
|
Navigators2["ELO"] = "elo";
|
|
3241
3720
|
Navigators2["SRS"] = "srs";
|
|
3721
|
+
Navigators2["PRESCRIBED"] = "prescribed";
|
|
3242
3722
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
3243
3723
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
3244
3724
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
@@ -3253,6 +3733,7 @@ var init_navigators = __esm({
|
|
|
3253
3733
|
NavigatorRoles = {
|
|
3254
3734
|
["elo" /* ELO */]: "generator" /* GENERATOR */,
|
|
3255
3735
|
["srs" /* SRS */]: "generator" /* GENERATOR */,
|
|
3736
|
+
["prescribed" /* PRESCRIBED */]: "generator" /* GENERATOR */,
|
|
3256
3737
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
3257
3738
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
3258
3739
|
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
@@ -3417,6 +3898,12 @@ var init_navigators = __esm({
|
|
|
3417
3898
|
async getWeightedCards(_limit) {
|
|
3418
3899
|
throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
|
|
3419
3900
|
}
|
|
3901
|
+
/**
|
|
3902
|
+
* Set ephemeral hints for the next pipeline run.
|
|
3903
|
+
* No-op for non-Pipeline navigators. Pipeline overrides this.
|
|
3904
|
+
*/
|
|
3905
|
+
setEphemeralHints(_hints) {
|
|
3906
|
+
}
|
|
3420
3907
|
};
|
|
3421
3908
|
}
|
|
3422
3909
|
});
|
|
@@ -3521,15 +4008,42 @@ var init_courseDB = __esm({
|
|
|
3521
4008
|
// private log(msg: string): void {
|
|
3522
4009
|
// log(`CourseLog: ${this.id}\n ${msg}`);
|
|
3523
4010
|
// }
|
|
4011
|
+
/**
|
|
4012
|
+
* Primary database handle used for all **read** operations (queries, gets).
|
|
4013
|
+
*
|
|
4014
|
+
* When local sync is active, this points to the local PouchDB replica for
|
|
4015
|
+
* fast, network-free reads. Otherwise it points to the remote CouchDB.
|
|
4016
|
+
*/
|
|
3524
4017
|
db;
|
|
4018
|
+
/**
|
|
4019
|
+
* Remote database handle used for all **write** operations.
|
|
4020
|
+
*
|
|
4021
|
+
* Always points to the remote CouchDB so that writes (ELO updates, tag
|
|
4022
|
+
* mutations, admin operations) aggregate on the server. The local replica
|
|
4023
|
+
* is a read-only snapshot that refreshes on the next page load.
|
|
4024
|
+
*
|
|
4025
|
+
* When local sync is NOT active, this is the same instance as `this.db`.
|
|
4026
|
+
*/
|
|
4027
|
+
remoteDB;
|
|
3525
4028
|
id;
|
|
3526
4029
|
_getCurrentUser;
|
|
3527
4030
|
updateQueue;
|
|
3528
|
-
|
|
4031
|
+
/**
|
|
4032
|
+
* @param id - Course ID
|
|
4033
|
+
* @param userLookup - Async function returning the current user DB
|
|
4034
|
+
* @param localDB - Optional local PouchDB replica for reads. When provided,
|
|
4035
|
+
* `this.db` uses the local replica and `this.remoteDB` stays remote.
|
|
4036
|
+
* The UpdateQueue reads from remote and writes to remote (local `_rev`
|
|
4037
|
+
* values may be stale, so read-modify-write cycles must go through
|
|
4038
|
+
* the remote DB to avoid conflicts).
|
|
4039
|
+
*/
|
|
4040
|
+
constructor(id, userLookup, localDB) {
|
|
3529
4041
|
this.id = id;
|
|
3530
|
-
|
|
4042
|
+
const remote = getCourseDB2(this.id);
|
|
4043
|
+
this.remoteDB = remote;
|
|
4044
|
+
this.db = localDB ?? remote;
|
|
3531
4045
|
this._getCurrentUser = userLookup;
|
|
3532
|
-
this.updateQueue = new UpdateQueue(this.
|
|
4046
|
+
this.updateQueue = new UpdateQueue(this.remoteDB, this.remoteDB);
|
|
3533
4047
|
}
|
|
3534
4048
|
getCourseID() {
|
|
3535
4049
|
return this.id;
|
|
@@ -3617,7 +4131,7 @@ var init_courseDB = __esm({
|
|
|
3617
4131
|
};
|
|
3618
4132
|
}
|
|
3619
4133
|
async removeCard(id) {
|
|
3620
|
-
const doc = await this.
|
|
4134
|
+
const doc = await this.remoteDB.get(id);
|
|
3621
4135
|
if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
|
|
3622
4136
|
throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
|
|
3623
4137
|
}
|
|
@@ -3638,7 +4152,7 @@ var init_courseDB = __esm({
|
|
|
3638
4152
|
} catch (error) {
|
|
3639
4153
|
logger.error(`Error removing card ${id} from tags: ${error}`);
|
|
3640
4154
|
}
|
|
3641
|
-
return this.
|
|
4155
|
+
return this.remoteDB.remove(doc);
|
|
3642
4156
|
}
|
|
3643
4157
|
async getCardDisplayableDataIDs(id) {
|
|
3644
4158
|
logger.debug(id.join(", "));
|
|
@@ -3740,8 +4254,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3740
4254
|
if (cardIds.length === 0) {
|
|
3741
4255
|
return /* @__PURE__ */ new Map();
|
|
3742
4256
|
}
|
|
3743
|
-
const
|
|
3744
|
-
const result = await db.query("getTags", {
|
|
4257
|
+
const result = await this.db.query("getTags", {
|
|
3745
4258
|
keys: cardIds,
|
|
3746
4259
|
include_docs: false
|
|
3747
4260
|
});
|
|
@@ -3758,6 +4271,14 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3758
4271
|
}
|
|
3759
4272
|
return tagsByCard;
|
|
3760
4273
|
}
|
|
4274
|
+
async getAllCardIds() {
|
|
4275
|
+
const result = await this.db.allDocs({
|
|
4276
|
+
startkey: "CARD-",
|
|
4277
|
+
endkey: "CARD-\uFFF0",
|
|
4278
|
+
include_docs: false
|
|
4279
|
+
});
|
|
4280
|
+
return result.rows.map((row) => row.id);
|
|
4281
|
+
}
|
|
3761
4282
|
async addTagToCard(cardId, tagId, updateELO) {
|
|
3762
4283
|
return await addTagToCard(
|
|
3763
4284
|
this.id,
|
|
@@ -3824,10 +4345,13 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3824
4345
|
}
|
|
3825
4346
|
}
|
|
3826
4347
|
async getCourseDoc(id, options) {
|
|
3827
|
-
return await
|
|
4348
|
+
return await this.db.get(id, options);
|
|
3828
4349
|
}
|
|
3829
4350
|
async getCourseDocs(ids, options = {}) {
|
|
3830
|
-
return await
|
|
4351
|
+
return await this.db.allDocs({
|
|
4352
|
+
...options,
|
|
4353
|
+
keys: ids
|
|
4354
|
+
});
|
|
3831
4355
|
}
|
|
3832
4356
|
////////////////////////////////////
|
|
3833
4357
|
// NavigationStrategyManager implementation
|
|
@@ -3861,7 +4385,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3861
4385
|
}
|
|
3862
4386
|
async addNavigationStrategy(data) {
|
|
3863
4387
|
logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
|
|
3864
|
-
return this.
|
|
4388
|
+
return this.remoteDB.put(data).then(() => {
|
|
3865
4389
|
});
|
|
3866
4390
|
}
|
|
3867
4391
|
updateNavigationStrategy(id, data) {
|
|
@@ -4277,6 +4801,16 @@ var init_adminDB2 = __esm({
|
|
|
4277
4801
|
}
|
|
4278
4802
|
});
|
|
4279
4803
|
|
|
4804
|
+
// src/impl/couch/CourseSyncService.ts
|
|
4805
|
+
var init_CourseSyncService = __esm({
|
|
4806
|
+
"src/impl/couch/CourseSyncService.ts"() {
|
|
4807
|
+
"use strict";
|
|
4808
|
+
init_pouchdb_setup();
|
|
4809
|
+
init_couch();
|
|
4810
|
+
init_logger();
|
|
4811
|
+
}
|
|
4812
|
+
});
|
|
4813
|
+
|
|
4280
4814
|
// src/impl/couch/auth.ts
|
|
4281
4815
|
import fetch from "cross-fetch";
|
|
4282
4816
|
var init_auth = __esm({
|
|
@@ -4331,15 +4865,6 @@ function getCourseDB2(courseID) {
|
|
|
4331
4865
|
createPouchDBConfig()
|
|
4332
4866
|
);
|
|
4333
4867
|
}
|
|
4334
|
-
function getCourseDocs(courseID, docIDs, options = {}) {
|
|
4335
|
-
return getCourseDB2(courseID).allDocs({
|
|
4336
|
-
...options,
|
|
4337
|
-
keys: docIDs
|
|
4338
|
-
});
|
|
4339
|
-
}
|
|
4340
|
-
function getCourseDoc(courseID, docID, options = {}) {
|
|
4341
|
-
return getCourseDB2(courseID).get(docID, options);
|
|
4342
|
-
}
|
|
4343
4868
|
function filterAllDocsByPrefix2(db, prefix, opts) {
|
|
4344
4869
|
const options = {
|
|
4345
4870
|
startkey: prefix,
|
|
@@ -4370,6 +4895,7 @@ var init_couch = __esm({
|
|
|
4370
4895
|
init_classroomDB2();
|
|
4371
4896
|
init_courseAPI();
|
|
4372
4897
|
init_courseDB();
|
|
4898
|
+
init_CourseSyncService();
|
|
4373
4899
|
init_CouchDBSyncStrategy();
|
|
4374
4900
|
isBrowser = typeof window !== "undefined";
|
|
4375
4901
|
if (isBrowser) {
|
|
@@ -4589,6 +5115,9 @@ Currently logged-in as ${this._username}.`
|
|
|
4589
5115
|
const id = row.id;
|
|
4590
5116
|
return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
|
|
4591
5117
|
id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
|
|
5118
|
+
id.startsWith(DocTypePrefixes["STRATEGY_STATE" /* STRATEGY_STATE */]) || // Strategy state (user prefs, progression)
|
|
5119
|
+
id.startsWith(DocTypePrefixes["USER_OUTCOME" /* USER_OUTCOME */]) || // Evolutionary orchestration outcomes
|
|
5120
|
+
id.startsWith(DocTypePrefixes["STRATEGY_LEARNING_STATE" /* STRATEGY_LEARNING_STATE */]) || // Strategy learning state
|
|
4592
5121
|
id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
4593
5122
|
id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
4594
5123
|
id === _BaseUser.DOC_IDS.CONFIG;
|
|
@@ -6153,6 +6682,7 @@ export {
|
|
|
6153
6682
|
getDefaultLearnableWeight,
|
|
6154
6683
|
getRegisteredNavigator,
|
|
6155
6684
|
getRegisteredNavigatorNames,
|
|
6685
|
+
getRegisteredNavigatorRole,
|
|
6156
6686
|
getStudySource,
|
|
6157
6687
|
hasRegisteredNavigator,
|
|
6158
6688
|
importParsedCards,
|