@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.js
CHANGED
|
@@ -725,8 +725,12 @@ __export(PipelineDebugger_exports, {
|
|
|
725
725
|
buildRunReport: () => buildRunReport,
|
|
726
726
|
captureRun: () => captureRun,
|
|
727
727
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
728
|
-
pipelineDebugAPI: () => pipelineDebugAPI
|
|
728
|
+
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
729
|
+
registerPipelineForDebug: () => registerPipelineForDebug
|
|
729
730
|
});
|
|
731
|
+
function registerPipelineForDebug(pipeline) {
|
|
732
|
+
_activePipeline = pipeline;
|
|
733
|
+
}
|
|
730
734
|
function getOrigin(card) {
|
|
731
735
|
const firstEntry = card.provenance[0];
|
|
732
736
|
if (!firstEntry) return "unknown";
|
|
@@ -754,6 +758,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
754
758
|
origin: getOrigin(card),
|
|
755
759
|
finalScore: card.score,
|
|
756
760
|
provenance: card.provenance,
|
|
761
|
+
tags: card.tags,
|
|
757
762
|
selected: selectedIds.has(card.cardId)
|
|
758
763
|
}));
|
|
759
764
|
const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
|
|
@@ -809,11 +814,13 @@ function mountPipelineDebugger() {
|
|
|
809
814
|
win.skuilder = win.skuilder || {};
|
|
810
815
|
win.skuilder.pipeline = pipelineDebugAPI;
|
|
811
816
|
}
|
|
812
|
-
var MAX_RUNS, runHistory, pipelineDebugAPI;
|
|
817
|
+
var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
|
|
813
818
|
var init_PipelineDebugger = __esm({
|
|
814
819
|
"src/core/navigators/PipelineDebugger.ts"() {
|
|
815
820
|
"use strict";
|
|
821
|
+
init_navigators();
|
|
816
822
|
init_logger();
|
|
823
|
+
_activePipeline = null;
|
|
817
824
|
MAX_RUNS = 10;
|
|
818
825
|
runHistory = [];
|
|
819
826
|
pipelineDebugAPI = {
|
|
@@ -955,6 +962,81 @@ var init_PipelineDebugger = __esm({
|
|
|
955
962
|
runHistory.length = 0;
|
|
956
963
|
logger.info("[Pipeline Debug] Run history cleared.");
|
|
957
964
|
},
|
|
965
|
+
/**
|
|
966
|
+
* Show the navigator registry: all registered classes and their roles.
|
|
967
|
+
*
|
|
968
|
+
* Useful for verifying that consumer-defined navigators were registered
|
|
969
|
+
* before pipeline assembly.
|
|
970
|
+
*/
|
|
971
|
+
showRegistry() {
|
|
972
|
+
const names = getRegisteredNavigatorNames();
|
|
973
|
+
if (names.length === 0) {
|
|
974
|
+
logger.info("[Pipeline Debug] Navigator registry is empty.");
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
console.group("\u{1F4E6} Navigator Registry");
|
|
978
|
+
console.table(
|
|
979
|
+
names.map((name) => {
|
|
980
|
+
const registryRole = getRegisteredNavigatorRole(name);
|
|
981
|
+
const builtinRole = NavigatorRoles[name];
|
|
982
|
+
const effectiveRole = builtinRole || registryRole || "\u26A0\uFE0F NONE";
|
|
983
|
+
const source = builtinRole ? "built-in" : registryRole ? "consumer" : "unclassified";
|
|
984
|
+
return {
|
|
985
|
+
name,
|
|
986
|
+
role: effectiveRole,
|
|
987
|
+
source,
|
|
988
|
+
isGenerator: isGenerator(name),
|
|
989
|
+
isFilter: isFilter(name)
|
|
990
|
+
};
|
|
991
|
+
})
|
|
992
|
+
);
|
|
993
|
+
console.groupEnd();
|
|
994
|
+
},
|
|
995
|
+
/**
|
|
996
|
+
* Show strategy documents from the last pipeline run and how they mapped
|
|
997
|
+
* to the registry.
|
|
998
|
+
*
|
|
999
|
+
* If no runs are captured yet, falls back to showing just the registry.
|
|
1000
|
+
*/
|
|
1001
|
+
showStrategies() {
|
|
1002
|
+
this.showRegistry();
|
|
1003
|
+
if (runHistory.length === 0) {
|
|
1004
|
+
logger.info("[Pipeline Debug] No pipeline runs captured yet \u2014 cannot show strategy doc mapping.");
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
const run = runHistory[0];
|
|
1008
|
+
console.group("\u{1F50C} Pipeline Strategy Mapping (last run)");
|
|
1009
|
+
logger.info(`Generator: ${run.generatorName}`);
|
|
1010
|
+
if (run.generators && run.generators.length > 0) {
|
|
1011
|
+
for (const g of run.generators) {
|
|
1012
|
+
logger.info(` \u{1F4E5} ${g.name}: ${g.cardCount} cards (${g.newCount} new, ${g.reviewCount} reviews)`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
if (run.filters.length > 0) {
|
|
1016
|
+
logger.info("Filters:");
|
|
1017
|
+
for (const f of run.filters) {
|
|
1018
|
+
logger.info(` \u{1F538} ${f.name}: \u2191${f.boosted} \u2193${f.penalized} =${f.passed} \u2715${f.removed}`);
|
|
1019
|
+
}
|
|
1020
|
+
} else {
|
|
1021
|
+
logger.info("Filters: (none)");
|
|
1022
|
+
}
|
|
1023
|
+
console.groupEnd();
|
|
1024
|
+
},
|
|
1025
|
+
/**
|
|
1026
|
+
* Scan the full card space through the filter chain for the current user.
|
|
1027
|
+
*
|
|
1028
|
+
* Reports how many cards are well-indicated and how many are new.
|
|
1029
|
+
* Use this to understand how the search space grows during onboarding.
|
|
1030
|
+
*
|
|
1031
|
+
* @param threshold - Score threshold for "well indicated" (default 0.10)
|
|
1032
|
+
*/
|
|
1033
|
+
async diagnoseCardSpace(threshold) {
|
|
1034
|
+
if (!_activePipeline) {
|
|
1035
|
+
logger.info("[Pipeline Debug] No active pipeline. Run a session first.");
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
return _activePipeline.diagnoseCardSpace({ threshold });
|
|
1039
|
+
},
|
|
958
1040
|
/**
|
|
959
1041
|
* Show help.
|
|
960
1042
|
*/
|
|
@@ -967,6 +1049,9 @@ Commands:
|
|
|
967
1049
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
968
1050
|
.showCard(cardId) Show provenance trail for a specific card
|
|
969
1051
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
1052
|
+
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
1053
|
+
.showRegistry() Show navigator registry (classes + roles)
|
|
1054
|
+
.showStrategies() Show registry + strategy mapping from last run
|
|
970
1055
|
.listRuns() List all captured runs in table format
|
|
971
1056
|
.export() Export run history as JSON for bug reports
|
|
972
1057
|
.clear() Clear run history
|
|
@@ -976,7 +1061,7 @@ Commands:
|
|
|
976
1061
|
Example:
|
|
977
1062
|
window.skuilder.pipeline.showLastRun()
|
|
978
1063
|
window.skuilder.pipeline.showRun(1)
|
|
979
|
-
window.skuilder.pipeline.
|
|
1064
|
+
await window.skuilder.pipeline.diagnoseCardSpace()
|
|
980
1065
|
`);
|
|
981
1066
|
}
|
|
982
1067
|
};
|
|
@@ -1271,6 +1356,69 @@ var init_generators = __esm({
|
|
|
1271
1356
|
}
|
|
1272
1357
|
});
|
|
1273
1358
|
|
|
1359
|
+
// src/core/navigators/generators/prescribed.ts
|
|
1360
|
+
var prescribed_exports = {};
|
|
1361
|
+
__export(prescribed_exports, {
|
|
1362
|
+
default: () => PrescribedCardsGenerator
|
|
1363
|
+
});
|
|
1364
|
+
var PrescribedCardsGenerator;
|
|
1365
|
+
var init_prescribed = __esm({
|
|
1366
|
+
"src/core/navigators/generators/prescribed.ts"() {
|
|
1367
|
+
"use strict";
|
|
1368
|
+
init_navigators();
|
|
1369
|
+
init_logger();
|
|
1370
|
+
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1371
|
+
name;
|
|
1372
|
+
config;
|
|
1373
|
+
constructor(user, course, strategyData) {
|
|
1374
|
+
super(user, course, strategyData);
|
|
1375
|
+
this.name = strategyData.name || "Prescribed Cards";
|
|
1376
|
+
try {
|
|
1377
|
+
const parsed = JSON.parse(strategyData.serializedData);
|
|
1378
|
+
this.config = { cardIds: parsed.cardIds || [] };
|
|
1379
|
+
} catch {
|
|
1380
|
+
this.config = { cardIds: [] };
|
|
1381
|
+
}
|
|
1382
|
+
logger.debug(
|
|
1383
|
+
`[Prescribed] Initialized with ${this.config.cardIds.length} prescribed cards`
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
async getWeightedCards(limit, _context) {
|
|
1387
|
+
if (this.config.cardIds.length === 0) {
|
|
1388
|
+
return [];
|
|
1389
|
+
}
|
|
1390
|
+
const courseId = this.course.getCourseID();
|
|
1391
|
+
const activeCards = await this.user.getActiveCards();
|
|
1392
|
+
const activeIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
1393
|
+
const eligibleIds = this.config.cardIds.filter((id) => !activeIds.has(id));
|
|
1394
|
+
if (eligibleIds.length === 0) {
|
|
1395
|
+
logger.debug("[Prescribed] All prescribed cards already active, returning empty");
|
|
1396
|
+
return [];
|
|
1397
|
+
}
|
|
1398
|
+
const cards = eligibleIds.slice(0, limit).map((cardId) => ({
|
|
1399
|
+
cardId,
|
|
1400
|
+
courseId,
|
|
1401
|
+
score: 1,
|
|
1402
|
+
provenance: [
|
|
1403
|
+
{
|
|
1404
|
+
strategy: "prescribed",
|
|
1405
|
+
strategyName: this.strategyName || this.name,
|
|
1406
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1407
|
+
action: "generated",
|
|
1408
|
+
score: 1,
|
|
1409
|
+
reason: `Prescribed card (${eligibleIds.length} eligible of ${this.config.cardIds.length} configured)`
|
|
1410
|
+
}
|
|
1411
|
+
]
|
|
1412
|
+
}));
|
|
1413
|
+
logger.info(
|
|
1414
|
+
`[Prescribed] Emitting ${cards.length} cards (${eligibleIds.length} eligible, ${activeIds.size} already active)`
|
|
1415
|
+
);
|
|
1416
|
+
return cards;
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
});
|
|
1421
|
+
|
|
1274
1422
|
// src/core/navigators/generators/srs.ts
|
|
1275
1423
|
var srs_exports = {};
|
|
1276
1424
|
__export(srs_exports, {
|
|
@@ -1465,6 +1613,7 @@ var init_ = __esm({
|
|
|
1465
1613
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
1466
1614
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
1467
1615
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
1616
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
1468
1617
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
1469
1618
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports))
|
|
1470
1619
|
});
|
|
@@ -1665,6 +1814,8 @@ var init_hierarchyDefinition = __esm({
|
|
|
1665
1814
|
if (userTagElo.count < minCount) return false;
|
|
1666
1815
|
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1667
1816
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
1817
|
+
} else if (prereq.masteryThreshold?.minCount !== void 0) {
|
|
1818
|
+
return true;
|
|
1668
1819
|
} else {
|
|
1669
1820
|
return userTagElo.score >= userGlobalElo;
|
|
1670
1821
|
}
|
|
@@ -1740,14 +1891,38 @@ var init_hierarchyDefinition = __esm({
|
|
|
1740
1891
|
};
|
|
1741
1892
|
}
|
|
1742
1893
|
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Build a map of prereq tag → max configured boost for all *closed* gates.
|
|
1896
|
+
*
|
|
1897
|
+
* When a gate is closed (prereqs unmet), cards carrying that gate's prereq
|
|
1898
|
+
* tags get boosted — steering the pipeline toward content that helps unlock
|
|
1899
|
+
* the gated material. Once the gate opens, the boost disappears.
|
|
1900
|
+
*/
|
|
1901
|
+
getPreReqBoosts(unlockedTags, masteredTags) {
|
|
1902
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
1903
|
+
for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
|
|
1904
|
+
if (unlockedTags.has(tagId)) continue;
|
|
1905
|
+
for (const prereq of prereqs) {
|
|
1906
|
+
if (!prereq.preReqBoost || prereq.preReqBoost <= 1) continue;
|
|
1907
|
+
if (masteredTags.has(prereq.tag)) continue;
|
|
1908
|
+
const existing = boosts.get(prereq.tag) ?? 1;
|
|
1909
|
+
boosts.set(prereq.tag, Math.max(existing, prereq.preReqBoost));
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
return boosts;
|
|
1913
|
+
}
|
|
1743
1914
|
/**
|
|
1744
1915
|
* CardFilter.transform implementation.
|
|
1745
1916
|
*
|
|
1746
|
-
*
|
|
1917
|
+
* Two effects:
|
|
1918
|
+
* 1. Cards with locked tags receive score * 0.05 (gating penalty)
|
|
1919
|
+
* 2. Cards carrying prereq tags of closed gates receive a configured
|
|
1920
|
+
* boost (preReqBoost), steering toward content that unlocks gates
|
|
1747
1921
|
*/
|
|
1748
1922
|
async transform(cards, context) {
|
|
1749
1923
|
const masteredTags = await this.getMasteredTags(context);
|
|
1750
1924
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
1925
|
+
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
1751
1926
|
const gated = [];
|
|
1752
1927
|
for (const card of cards) {
|
|
1753
1928
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
@@ -1756,9 +1931,27 @@ var init_hierarchyDefinition = __esm({
|
|
|
1756
1931
|
unlockedTags,
|
|
1757
1932
|
masteredTags
|
|
1758
1933
|
);
|
|
1759
|
-
const LOCKED_PENALTY = 0.
|
|
1760
|
-
|
|
1761
|
-
|
|
1934
|
+
const LOCKED_PENALTY = 0.02;
|
|
1935
|
+
let finalScore = isUnlocked ? card.score : card.score * LOCKED_PENALTY;
|
|
1936
|
+
let action = isUnlocked ? "passed" : "penalized";
|
|
1937
|
+
let finalReason = reason;
|
|
1938
|
+
if (isUnlocked && preReqBoosts.size > 0) {
|
|
1939
|
+
const cardTags = card.tags ?? [];
|
|
1940
|
+
let maxBoost = 1;
|
|
1941
|
+
const boostedPrereqs = [];
|
|
1942
|
+
for (const tag of cardTags) {
|
|
1943
|
+
const boost = preReqBoosts.get(tag);
|
|
1944
|
+
if (boost && boost > maxBoost) {
|
|
1945
|
+
maxBoost = boost;
|
|
1946
|
+
boostedPrereqs.push(tag);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
if (maxBoost > 1) {
|
|
1950
|
+
finalScore *= maxBoost;
|
|
1951
|
+
action = "boosted";
|
|
1952
|
+
finalReason = `${reason} | preReqBoost \xD7${maxBoost.toFixed(2)} for ${boostedPrereqs.join(", ")}`;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1762
1955
|
gated.push({
|
|
1763
1956
|
...card,
|
|
1764
1957
|
score: finalScore,
|
|
@@ -1770,7 +1963,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1770
1963
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-hierarchy",
|
|
1771
1964
|
action,
|
|
1772
1965
|
score: finalScore,
|
|
1773
|
-
reason
|
|
1966
|
+
reason: finalReason
|
|
1774
1967
|
}
|
|
1775
1968
|
]
|
|
1776
1969
|
});
|
|
@@ -2704,6 +2897,18 @@ var Pipeline_exports = {};
|
|
|
2704
2897
|
__export(Pipeline_exports, {
|
|
2705
2898
|
Pipeline: () => Pipeline
|
|
2706
2899
|
});
|
|
2900
|
+
function globToRegex(pattern) {
|
|
2901
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
2902
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
2903
|
+
return new RegExp(`^${withWildcards}$`);
|
|
2904
|
+
}
|
|
2905
|
+
function globMatch(value, pattern) {
|
|
2906
|
+
if (!pattern.includes("*")) return value === pattern;
|
|
2907
|
+
return globToRegex(pattern).test(value);
|
|
2908
|
+
}
|
|
2909
|
+
function cardMatchesTagPattern(card, pattern) {
|
|
2910
|
+
return (card.tags ?? []).some((tag) => globMatch(tag, pattern));
|
|
2911
|
+
}
|
|
2707
2912
|
function logPipelineConfig(generator, filters) {
|
|
2708
2913
|
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
2709
2914
|
logger.info(
|
|
@@ -2738,6 +2943,21 @@ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCo
|
|
|
2738
2943
|
\u{1F4A1} Inspect: window.skuilder.pipeline`
|
|
2739
2944
|
);
|
|
2740
2945
|
}
|
|
2946
|
+
function logResultCards(cards) {
|
|
2947
|
+
if (!VERBOSE_RESULTS || cards.length === 0) return;
|
|
2948
|
+
logger.info(`[Pipeline] Results (${cards.length} cards):`);
|
|
2949
|
+
for (let i = 0; i < cards.length; i++) {
|
|
2950
|
+
const c = cards[i];
|
|
2951
|
+
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
2952
|
+
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
2953
|
+
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
2954
|
+
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
2955
|
+
}).join(" | ");
|
|
2956
|
+
logger.info(
|
|
2957
|
+
`[Pipeline] ${String(i + 1).padStart(2)}. ${c.score.toFixed(4)} ${c.cardId} [${tags}]${filters ? ` {${filters}}` : ""}`
|
|
2958
|
+
);
|
|
2959
|
+
}
|
|
2960
|
+
}
|
|
2741
2961
|
function logCardProvenance(cards, maxCards = 3) {
|
|
2742
2962
|
const cardsToLog = cards.slice(0, maxCards);
|
|
2743
2963
|
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
@@ -2752,7 +2972,7 @@ function logCardProvenance(cards, maxCards = 3) {
|
|
|
2752
2972
|
}
|
|
2753
2973
|
}
|
|
2754
2974
|
}
|
|
2755
|
-
var import_common8, Pipeline;
|
|
2975
|
+
var import_common8, VERBOSE_RESULTS, Pipeline;
|
|
2756
2976
|
var init_Pipeline = __esm({
|
|
2757
2977
|
"src/core/navigators/Pipeline.ts"() {
|
|
2758
2978
|
"use strict";
|
|
@@ -2761,9 +2981,31 @@ var init_Pipeline = __esm({
|
|
|
2761
2981
|
init_logger();
|
|
2762
2982
|
init_orchestration();
|
|
2763
2983
|
init_PipelineDebugger();
|
|
2984
|
+
VERBOSE_RESULTS = true;
|
|
2764
2985
|
Pipeline = class extends ContentNavigator {
|
|
2765
2986
|
generator;
|
|
2766
2987
|
filters;
|
|
2988
|
+
/**
|
|
2989
|
+
* Cached orchestration context. Course config and salt don't change within
|
|
2990
|
+
* a page load, so we build the orchestration context once and reuse it on
|
|
2991
|
+
* subsequent getWeightedCards() calls (e.g. mid-session replans).
|
|
2992
|
+
*
|
|
2993
|
+
* This eliminates a remote getCourseConfig() round trip per pipeline run.
|
|
2994
|
+
*/
|
|
2995
|
+
_cachedOrchestration = null;
|
|
2996
|
+
/**
|
|
2997
|
+
* Persistent tag cache. Maps cardId → tag names.
|
|
2998
|
+
*
|
|
2999
|
+
* Tags are static within a session (they're set at card generation time),
|
|
3000
|
+
* so we cache them across pipeline runs. On replans, many of the same cards
|
|
3001
|
+
* reappear — cache hits avoid redundant remote getAppliedTagsBatch() queries.
|
|
3002
|
+
*/
|
|
3003
|
+
_tagCache = /* @__PURE__ */ new Map();
|
|
3004
|
+
/**
|
|
3005
|
+
* One-shot replan hints. Applied after the filter chain on the next
|
|
3006
|
+
* getWeightedCards() call, then cleared.
|
|
3007
|
+
*/
|
|
3008
|
+
_ephemeralHints = null;
|
|
2767
3009
|
/**
|
|
2768
3010
|
* Create a new pipeline.
|
|
2769
3011
|
*
|
|
@@ -2784,6 +3026,17 @@ var init_Pipeline = __esm({
|
|
|
2784
3026
|
logger.error(`[pipeline] Failed to lookup courseCfg: ${e}`);
|
|
2785
3027
|
});
|
|
2786
3028
|
logPipelineConfig(generator, filters);
|
|
3029
|
+
registerPipelineForDebug(this);
|
|
3030
|
+
}
|
|
3031
|
+
/**
|
|
3032
|
+
* Set one-shot hints for the next pipeline run.
|
|
3033
|
+
* Consumed after one getWeightedCards() call, then cleared.
|
|
3034
|
+
*
|
|
3035
|
+
* Overrides ContentNavigator.setEphemeralHints() no-op.
|
|
3036
|
+
*/
|
|
3037
|
+
setEphemeralHints(hints) {
|
|
3038
|
+
this._ephemeralHints = hints;
|
|
3039
|
+
logger.info(`[Pipeline] Ephemeral hints set: ${JSON.stringify(hints)}`);
|
|
2787
3040
|
}
|
|
2788
3041
|
/**
|
|
2789
3042
|
* Get weighted cards by running generator and applying filters.
|
|
@@ -2800,13 +3053,15 @@ var init_Pipeline = __esm({
|
|
|
2800
3053
|
* @returns Cards sorted by score descending
|
|
2801
3054
|
*/
|
|
2802
3055
|
async getWeightedCards(limit) {
|
|
3056
|
+
const t0 = performance.now();
|
|
2803
3057
|
const context = await this.buildContext();
|
|
2804
|
-
const
|
|
2805
|
-
const fetchLimit =
|
|
3058
|
+
const tContext = performance.now();
|
|
3059
|
+
const fetchLimit = 500;
|
|
2806
3060
|
logger.debug(
|
|
2807
3061
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
2808
3062
|
);
|
|
2809
3063
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
3064
|
+
const tGenerate = performance.now();
|
|
2810
3065
|
const generatedCount = cards.length;
|
|
2811
3066
|
let generatorSummaries;
|
|
2812
3067
|
if (this.generator.generators) {
|
|
@@ -2835,6 +3090,7 @@ var init_Pipeline = __esm({
|
|
|
2835
3090
|
}
|
|
2836
3091
|
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
2837
3092
|
cards = await this.hydrateTags(cards);
|
|
3093
|
+
const tHydrate = performance.now();
|
|
2838
3094
|
const allCardsBeforeFiltering = [...cards];
|
|
2839
3095
|
const filterImpacts = [];
|
|
2840
3096
|
for (const filter of this.filters) {
|
|
@@ -2853,8 +3109,17 @@ var init_Pipeline = __esm({
|
|
|
2853
3109
|
logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
|
|
2854
3110
|
}
|
|
2855
3111
|
cards = cards.filter((c) => c.score > 0);
|
|
3112
|
+
const hints = this._ephemeralHints;
|
|
3113
|
+
if (hints) {
|
|
3114
|
+
this._ephemeralHints = null;
|
|
3115
|
+
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
3116
|
+
}
|
|
2856
3117
|
cards.sort((a, b) => b.score - a.score);
|
|
3118
|
+
const tFilter = performance.now();
|
|
2857
3119
|
const result = cards.slice(0, limit);
|
|
3120
|
+
logger.info(
|
|
3121
|
+
`[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)})`
|
|
3122
|
+
);
|
|
2858
3123
|
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
2859
3124
|
logExecutionSummary(
|
|
2860
3125
|
this.generator.name,
|
|
@@ -2864,6 +3129,7 @@ var init_Pipeline = __esm({
|
|
|
2864
3129
|
topScores,
|
|
2865
3130
|
filterImpacts
|
|
2866
3131
|
);
|
|
3132
|
+
logResultCards(result);
|
|
2867
3133
|
logCardProvenance(result, 3);
|
|
2868
3134
|
try {
|
|
2869
3135
|
const courseName = await this.course?.getCourseConfig().then((c) => c.name).catch(() => void 0);
|
|
@@ -2890,6 +3156,10 @@ var init_Pipeline = __esm({
|
|
|
2890
3156
|
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
2891
3157
|
* making individual getAppliedTags() calls.
|
|
2892
3158
|
*
|
|
3159
|
+
* Uses a persistent tag cache across pipeline runs — tags are static within
|
|
3160
|
+
* a session, so cards seen in a prior run (e.g. before a replan) don't
|
|
3161
|
+
* require a second DB query.
|
|
3162
|
+
*
|
|
2893
3163
|
* @param cards - Cards to hydrate
|
|
2894
3164
|
* @returns Cards with tags populated
|
|
2895
3165
|
*/
|
|
@@ -2897,14 +3167,128 @@ var init_Pipeline = __esm({
|
|
|
2897
3167
|
if (cards.length === 0) {
|
|
2898
3168
|
return cards;
|
|
2899
3169
|
}
|
|
2900
|
-
const
|
|
2901
|
-
const
|
|
3170
|
+
const uncachedIds = [];
|
|
3171
|
+
for (const card of cards) {
|
|
3172
|
+
if (!this._tagCache.has(card.cardId)) {
|
|
3173
|
+
uncachedIds.push(card.cardId);
|
|
3174
|
+
}
|
|
3175
|
+
}
|
|
3176
|
+
if (uncachedIds.length > 0) {
|
|
3177
|
+
const freshTags = await this.course.getAppliedTagsBatch(uncachedIds);
|
|
3178
|
+
for (const [cardId, tags] of freshTags) {
|
|
3179
|
+
this._tagCache.set(cardId, tags);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
3183
|
+
for (const card of cards) {
|
|
3184
|
+
tagsByCard.set(card.cardId, this._tagCache.get(card.cardId) ?? []);
|
|
3185
|
+
}
|
|
2902
3186
|
logTagHydration(cards, tagsByCard);
|
|
2903
3187
|
return cards.map((card) => ({
|
|
2904
3188
|
...card,
|
|
2905
|
-
tags:
|
|
3189
|
+
tags: this._tagCache.get(card.cardId) ?? []
|
|
2906
3190
|
}));
|
|
2907
3191
|
}
|
|
3192
|
+
// ---------------------------------------------------------------------------
|
|
3193
|
+
// Ephemeral hints application
|
|
3194
|
+
// ---------------------------------------------------------------------------
|
|
3195
|
+
/**
|
|
3196
|
+
* Apply one-shot replan hints to the post-filter card set.
|
|
3197
|
+
*
|
|
3198
|
+
* Order of operations:
|
|
3199
|
+
* 1. Exclude (remove unwanted cards)
|
|
3200
|
+
* 2. Boost (multiply scores)
|
|
3201
|
+
* 3. Require (inject must-have cards from the full pre-filter pool)
|
|
3202
|
+
*
|
|
3203
|
+
* @param cards - Post-filter cards (score > 0)
|
|
3204
|
+
* @param hints - The ephemeral hints to apply
|
|
3205
|
+
* @param allCards - Full pre-filter card pool (for require injection)
|
|
3206
|
+
*/
|
|
3207
|
+
applyHints(cards, hints, allCards) {
|
|
3208
|
+
const beforeCount = cards.length;
|
|
3209
|
+
if (hints.excludeCards?.length) {
|
|
3210
|
+
cards = cards.filter(
|
|
3211
|
+
(c) => !hints.excludeCards.some((pat) => globMatch(c.cardId, pat))
|
|
3212
|
+
);
|
|
3213
|
+
}
|
|
3214
|
+
if (hints.excludeTags?.length) {
|
|
3215
|
+
cards = cards.filter(
|
|
3216
|
+
(c) => !hints.excludeTags.some((pat) => cardMatchesTagPattern(c, pat))
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
3219
|
+
if (hints.boostTags) {
|
|
3220
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags)) {
|
|
3221
|
+
for (const card of cards) {
|
|
3222
|
+
if (cardMatchesTagPattern(card, pattern)) {
|
|
3223
|
+
card.score *= factor;
|
|
3224
|
+
card.provenance.push({
|
|
3225
|
+
strategy: "ephemeralHint",
|
|
3226
|
+
strategyId: "ephemeral-hint",
|
|
3227
|
+
strategyName: "Replan Hint",
|
|
3228
|
+
action: "boosted",
|
|
3229
|
+
score: card.score,
|
|
3230
|
+
reason: `boostTag ${pattern} \xD7${factor}`
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
}
|
|
3235
|
+
}
|
|
3236
|
+
if (hints.boostCards) {
|
|
3237
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards)) {
|
|
3238
|
+
for (const card of cards) {
|
|
3239
|
+
if (globMatch(card.cardId, pattern)) {
|
|
3240
|
+
card.score *= factor;
|
|
3241
|
+
card.provenance.push({
|
|
3242
|
+
strategy: "ephemeralHint",
|
|
3243
|
+
strategyId: "ephemeral-hint",
|
|
3244
|
+
strategyName: "Replan Hint",
|
|
3245
|
+
action: "boosted",
|
|
3246
|
+
score: card.score,
|
|
3247
|
+
reason: `boostCard ${pattern} \xD7${factor}`
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
const cardIds = new Set(cards.map((c) => c.cardId));
|
|
3254
|
+
const inject = (card, reason) => {
|
|
3255
|
+
if (!cardIds.has(card.cardId)) {
|
|
3256
|
+
const floorScore = Math.max(card.score, 1);
|
|
3257
|
+
cards.push({
|
|
3258
|
+
...card,
|
|
3259
|
+
score: floorScore,
|
|
3260
|
+
provenance: [
|
|
3261
|
+
...card.provenance,
|
|
3262
|
+
{
|
|
3263
|
+
strategy: "ephemeralHint",
|
|
3264
|
+
strategyId: "ephemeral-hint",
|
|
3265
|
+
strategyName: "Replan Hint",
|
|
3266
|
+
action: "boosted",
|
|
3267
|
+
score: floorScore,
|
|
3268
|
+
reason
|
|
3269
|
+
}
|
|
3270
|
+
]
|
|
3271
|
+
});
|
|
3272
|
+
cardIds.add(card.cardId);
|
|
3273
|
+
}
|
|
3274
|
+
};
|
|
3275
|
+
if (hints.requireCards?.length) {
|
|
3276
|
+
for (const pattern of hints.requireCards) {
|
|
3277
|
+
for (const card of allCards) {
|
|
3278
|
+
if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
if (hints.requireTags?.length) {
|
|
3283
|
+
for (const pattern of hints.requireTags) {
|
|
3284
|
+
for (const card of allCards) {
|
|
3285
|
+
if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
|
|
3286
|
+
}
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
logger.info(`[Pipeline] Hints applied: ${beforeCount} \u2192 ${cards.length} cards`);
|
|
3290
|
+
return cards;
|
|
3291
|
+
}
|
|
2908
3292
|
/**
|
|
2909
3293
|
* Build shared context for generator and filters.
|
|
2910
3294
|
*
|
|
@@ -2922,7 +3306,10 @@ var init_Pipeline = __esm({
|
|
|
2922
3306
|
} catch (e) {
|
|
2923
3307
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
2924
3308
|
}
|
|
2925
|
-
|
|
3309
|
+
if (!this._cachedOrchestration) {
|
|
3310
|
+
this._cachedOrchestration = await createOrchestrationContext(this.user, this.course);
|
|
3311
|
+
}
|
|
3312
|
+
const orchestration = this._cachedOrchestration;
|
|
2926
3313
|
return {
|
|
2927
3314
|
user: this.user,
|
|
2928
3315
|
course: this.course,
|
|
@@ -2966,6 +3353,87 @@ var init_Pipeline = __esm({
|
|
|
2966
3353
|
}
|
|
2967
3354
|
return [...new Set(ids)];
|
|
2968
3355
|
}
|
|
3356
|
+
// ---------------------------------------------------------------------------
|
|
3357
|
+
// Card-space diagnostic
|
|
3358
|
+
// ---------------------------------------------------------------------------
|
|
3359
|
+
/**
|
|
3360
|
+
* Scan every card in the course through the filter chain and report
|
|
3361
|
+
* how many are "well indicated" (score >= threshold) for the current user.
|
|
3362
|
+
*
|
|
3363
|
+
* Also reports how many well-indicated cards the user has NOT yet encountered.
|
|
3364
|
+
*
|
|
3365
|
+
* Exposed via `window.skuilder.pipeline.diagnoseCardSpace()`.
|
|
3366
|
+
*/
|
|
3367
|
+
async diagnoseCardSpace(opts) {
|
|
3368
|
+
const THRESHOLD = opts?.threshold ?? 0.1;
|
|
3369
|
+
const t0 = performance.now();
|
|
3370
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
3371
|
+
let cards = allCardIds.map((cardId) => ({
|
|
3372
|
+
cardId,
|
|
3373
|
+
courseId: this.course.getCourseID(),
|
|
3374
|
+
score: 1,
|
|
3375
|
+
provenance: []
|
|
3376
|
+
}));
|
|
3377
|
+
cards = await this.hydrateTags(cards);
|
|
3378
|
+
const context = await this.buildContext();
|
|
3379
|
+
const filterBreakdown = [];
|
|
3380
|
+
for (const filter of this.filters) {
|
|
3381
|
+
cards = await filter.transform(cards, context);
|
|
3382
|
+
const wi = cards.filter((c) => c.score >= THRESHOLD).length;
|
|
3383
|
+
filterBreakdown.push({ name: filter.name, wellIndicated: wi });
|
|
3384
|
+
}
|
|
3385
|
+
const wellIndicated = cards.filter((c) => c.score >= THRESHOLD);
|
|
3386
|
+
const wellIndicatedIds = new Set(wellIndicated.map((c) => c.cardId));
|
|
3387
|
+
let encounteredIds;
|
|
3388
|
+
try {
|
|
3389
|
+
const courseId = this.course.getCourseID();
|
|
3390
|
+
const seenCards = await this.user.getSeenCards(courseId);
|
|
3391
|
+
encounteredIds = new Set(seenCards);
|
|
3392
|
+
} catch {
|
|
3393
|
+
encounteredIds = /* @__PURE__ */ new Set();
|
|
3394
|
+
}
|
|
3395
|
+
const wellIndicatedNew = wellIndicated.filter((c) => !encounteredIds.has(c.cardId));
|
|
3396
|
+
const byType = /* @__PURE__ */ new Map();
|
|
3397
|
+
for (const card of cards) {
|
|
3398
|
+
const type = card.cardId.split("-")[1] || "unknown";
|
|
3399
|
+
if (!byType.has(type)) {
|
|
3400
|
+
byType.set(type, { total: 0, wellIndicated: 0, new: 0 });
|
|
3401
|
+
}
|
|
3402
|
+
const entry = byType.get(type);
|
|
3403
|
+
entry.total++;
|
|
3404
|
+
if (card.score >= THRESHOLD) {
|
|
3405
|
+
entry.wellIndicated++;
|
|
3406
|
+
if (!encounteredIds.has(card.cardId)) entry.new++;
|
|
3407
|
+
}
|
|
3408
|
+
}
|
|
3409
|
+
const elapsed = performance.now() - t0;
|
|
3410
|
+
const result = {
|
|
3411
|
+
totalCards: allCardIds.length,
|
|
3412
|
+
threshold: THRESHOLD,
|
|
3413
|
+
wellIndicated: wellIndicatedIds.size,
|
|
3414
|
+
encountered: encounteredIds.size,
|
|
3415
|
+
wellIndicatedNew: wellIndicatedNew.length,
|
|
3416
|
+
byType: Object.fromEntries(byType),
|
|
3417
|
+
filterBreakdown,
|
|
3418
|
+
elapsedMs: Math.round(elapsed)
|
|
3419
|
+
};
|
|
3420
|
+
logger.info(`[Pipeline:diagnose] Card space scan (${result.elapsedMs}ms):`);
|
|
3421
|
+
logger.info(`[Pipeline:diagnose] Total cards: ${result.totalCards}`);
|
|
3422
|
+
logger.info(`[Pipeline:diagnose] Well-indicated (score >= ${THRESHOLD}): ${result.wellIndicated}`);
|
|
3423
|
+
logger.info(`[Pipeline:diagnose] Encountered: ${result.encountered}`);
|
|
3424
|
+
logger.info(`[Pipeline:diagnose] Well-indicated & new: ${result.wellIndicatedNew}`);
|
|
3425
|
+
logger.info(`[Pipeline:diagnose] By type:`);
|
|
3426
|
+
for (const [type, counts] of byType) {
|
|
3427
|
+
logger.info(
|
|
3428
|
+
`[Pipeline:diagnose] ${type}: ${counts.wellIndicated}/${counts.total} well-indicated, ${counts.new} new`
|
|
3429
|
+
);
|
|
3430
|
+
}
|
|
3431
|
+
logger.info(`[Pipeline:diagnose] After each filter:`);
|
|
3432
|
+
for (const fb of filterBreakdown) {
|
|
3433
|
+
logger.info(`[Pipeline:diagnose] ${fb.name}: ${fb.wellIndicated} well-indicated`);
|
|
3434
|
+
}
|
|
3435
|
+
return result;
|
|
3436
|
+
}
|
|
2969
3437
|
};
|
|
2970
3438
|
}
|
|
2971
3439
|
});
|
|
@@ -3070,23 +3538,25 @@ var init_PipelineAssembler = __esm({
|
|
|
3070
3538
|
warnings.push(`Unknown strategy type '${s.implementingClass}', skipping: ${s.name}`);
|
|
3071
3539
|
}
|
|
3072
3540
|
}
|
|
3541
|
+
const courseId = course.getCourseID();
|
|
3542
|
+
const hasElo = generatorStrategies.some((s) => s.implementingClass === "elo" /* ELO */);
|
|
3543
|
+
const hasSrs = generatorStrategies.some((s) => s.implementingClass === "srs" /* SRS */);
|
|
3544
|
+
if (!hasElo) {
|
|
3545
|
+
logger.debug("[PipelineAssembler] No ELO generator configured, adding default");
|
|
3546
|
+
generatorStrategies.push(createDefaultEloStrategy(courseId));
|
|
3547
|
+
}
|
|
3548
|
+
if (!hasSrs) {
|
|
3549
|
+
logger.debug("[PipelineAssembler] No SRS generator configured, adding default");
|
|
3550
|
+
generatorStrategies.push(createDefaultSrsStrategy(courseId));
|
|
3551
|
+
}
|
|
3073
3552
|
if (generatorStrategies.length === 0) {
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
} else {
|
|
3082
|
-
warnings.push("No generator strategy found");
|
|
3083
|
-
return {
|
|
3084
|
-
pipeline: null,
|
|
3085
|
-
generatorStrategies: [],
|
|
3086
|
-
filterStrategies: [],
|
|
3087
|
-
warnings
|
|
3088
|
-
};
|
|
3089
|
-
}
|
|
3553
|
+
warnings.push("No generator strategy found");
|
|
3554
|
+
return {
|
|
3555
|
+
pipeline: null,
|
|
3556
|
+
generatorStrategies: [],
|
|
3557
|
+
filterStrategies: [],
|
|
3558
|
+
warnings
|
|
3559
|
+
};
|
|
3090
3560
|
}
|
|
3091
3561
|
let generator;
|
|
3092
3562
|
if (generatorStrategies.length === 1) {
|
|
@@ -3164,6 +3634,7 @@ var init_3 = __esm({
|
|
|
3164
3634
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
3165
3635
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
3166
3636
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
3637
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
3167
3638
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
3168
3639
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
3169
3640
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
@@ -3181,6 +3652,7 @@ __export(navigators_exports, {
|
|
|
3181
3652
|
getCardOrigin: () => getCardOrigin,
|
|
3182
3653
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
3183
3654
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
3655
|
+
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
3184
3656
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
3185
3657
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
3186
3658
|
isFilter: () => isFilter,
|
|
@@ -3189,16 +3661,19 @@ __export(navigators_exports, {
|
|
|
3189
3661
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
3190
3662
|
registerNavigator: () => registerNavigator
|
|
3191
3663
|
});
|
|
3192
|
-
function registerNavigator(implementingClass, constructor) {
|
|
3193
|
-
navigatorRegistry.set(implementingClass, constructor);
|
|
3194
|
-
logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}`);
|
|
3664
|
+
function registerNavigator(implementingClass, constructor, role) {
|
|
3665
|
+
navigatorRegistry.set(implementingClass, { constructor, role });
|
|
3666
|
+
logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}${role ? ` (${role})` : ""}`);
|
|
3195
3667
|
}
|
|
3196
3668
|
function getRegisteredNavigator(implementingClass) {
|
|
3197
|
-
return navigatorRegistry.get(implementingClass);
|
|
3669
|
+
return navigatorRegistry.get(implementingClass)?.constructor;
|
|
3198
3670
|
}
|
|
3199
3671
|
function hasRegisteredNavigator(implementingClass) {
|
|
3200
3672
|
return navigatorRegistry.has(implementingClass);
|
|
3201
3673
|
}
|
|
3674
|
+
function getRegisteredNavigatorRole(implementingClass) {
|
|
3675
|
+
return navigatorRegistry.get(implementingClass)?.role;
|
|
3676
|
+
}
|
|
3202
3677
|
function getRegisteredNavigatorNames() {
|
|
3203
3678
|
return Array.from(navigatorRegistry.keys());
|
|
3204
3679
|
}
|
|
@@ -3208,8 +3683,10 @@ async function initializeNavigatorRegistry() {
|
|
|
3208
3683
|
Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
3209
3684
|
Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
3210
3685
|
]);
|
|
3686
|
+
const prescribedModule = await Promise.resolve().then(() => (init_prescribed(), prescribed_exports));
|
|
3211
3687
|
registerNavigator("elo", eloModule.default);
|
|
3212
3688
|
registerNavigator("srs", srsModule.default);
|
|
3689
|
+
registerNavigator("prescribed", prescribedModule.default);
|
|
3213
3690
|
const [
|
|
3214
3691
|
hierarchyModule,
|
|
3215
3692
|
interferenceModule,
|
|
@@ -3244,10 +3721,12 @@ function getCardOrigin(card) {
|
|
|
3244
3721
|
return "new";
|
|
3245
3722
|
}
|
|
3246
3723
|
function isGenerator(impl) {
|
|
3247
|
-
|
|
3724
|
+
if (NavigatorRoles[impl] === "generator" /* GENERATOR */) return true;
|
|
3725
|
+
return getRegisteredNavigatorRole(impl) === "generator" /* GENERATOR */;
|
|
3248
3726
|
}
|
|
3249
3727
|
function isFilter(impl) {
|
|
3250
|
-
|
|
3728
|
+
if (NavigatorRoles[impl] === "filter" /* FILTER */) return true;
|
|
3729
|
+
return getRegisteredNavigatorRole(impl) === "filter" /* FILTER */;
|
|
3251
3730
|
}
|
|
3252
3731
|
var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigator;
|
|
3253
3732
|
var init_navigators = __esm({
|
|
@@ -3262,6 +3741,7 @@ var init_navigators = __esm({
|
|
|
3262
3741
|
Navigators = /* @__PURE__ */ ((Navigators2) => {
|
|
3263
3742
|
Navigators2["ELO"] = "elo";
|
|
3264
3743
|
Navigators2["SRS"] = "srs";
|
|
3744
|
+
Navigators2["PRESCRIBED"] = "prescribed";
|
|
3265
3745
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
3266
3746
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
3267
3747
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
@@ -3276,6 +3756,7 @@ var init_navigators = __esm({
|
|
|
3276
3756
|
NavigatorRoles = {
|
|
3277
3757
|
["elo" /* ELO */]: "generator" /* GENERATOR */,
|
|
3278
3758
|
["srs" /* SRS */]: "generator" /* GENERATOR */,
|
|
3759
|
+
["prescribed" /* PRESCRIBED */]: "generator" /* GENERATOR */,
|
|
3279
3760
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
3280
3761
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
3281
3762
|
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
@@ -3440,6 +3921,12 @@ var init_navigators = __esm({
|
|
|
3440
3921
|
async getWeightedCards(_limit) {
|
|
3441
3922
|
throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
|
|
3442
3923
|
}
|
|
3924
|
+
/**
|
|
3925
|
+
* Set ephemeral hints for the next pipeline run.
|
|
3926
|
+
* No-op for non-Pipeline navigators. Pipeline overrides this.
|
|
3927
|
+
*/
|
|
3928
|
+
setEphemeralHints(_hints) {
|
|
3929
|
+
}
|
|
3443
3930
|
};
|
|
3444
3931
|
}
|
|
3445
3932
|
});
|
|
@@ -3539,15 +4026,42 @@ var init_courseDB = __esm({
|
|
|
3539
4026
|
// private log(msg: string): void {
|
|
3540
4027
|
// log(`CourseLog: ${this.id}\n ${msg}`);
|
|
3541
4028
|
// }
|
|
4029
|
+
/**
|
|
4030
|
+
* Primary database handle used for all **read** operations (queries, gets).
|
|
4031
|
+
*
|
|
4032
|
+
* When local sync is active, this points to the local PouchDB replica for
|
|
4033
|
+
* fast, network-free reads. Otherwise it points to the remote CouchDB.
|
|
4034
|
+
*/
|
|
3542
4035
|
db;
|
|
4036
|
+
/**
|
|
4037
|
+
* Remote database handle used for all **write** operations.
|
|
4038
|
+
*
|
|
4039
|
+
* Always points to the remote CouchDB so that writes (ELO updates, tag
|
|
4040
|
+
* mutations, admin operations) aggregate on the server. The local replica
|
|
4041
|
+
* is a read-only snapshot that refreshes on the next page load.
|
|
4042
|
+
*
|
|
4043
|
+
* When local sync is NOT active, this is the same instance as `this.db`.
|
|
4044
|
+
*/
|
|
4045
|
+
remoteDB;
|
|
3543
4046
|
id;
|
|
3544
4047
|
_getCurrentUser;
|
|
3545
4048
|
updateQueue;
|
|
3546
|
-
|
|
4049
|
+
/**
|
|
4050
|
+
* @param id - Course ID
|
|
4051
|
+
* @param userLookup - Async function returning the current user DB
|
|
4052
|
+
* @param localDB - Optional local PouchDB replica for reads. When provided,
|
|
4053
|
+
* `this.db` uses the local replica and `this.remoteDB` stays remote.
|
|
4054
|
+
* The UpdateQueue reads from remote and writes to remote (local `_rev`
|
|
4055
|
+
* values may be stale, so read-modify-write cycles must go through
|
|
4056
|
+
* the remote DB to avoid conflicts).
|
|
4057
|
+
*/
|
|
4058
|
+
constructor(id, userLookup, localDB) {
|
|
3547
4059
|
this.id = id;
|
|
3548
|
-
|
|
4060
|
+
const remote = getCourseDB2(this.id);
|
|
4061
|
+
this.remoteDB = remote;
|
|
4062
|
+
this.db = localDB ?? remote;
|
|
3549
4063
|
this._getCurrentUser = userLookup;
|
|
3550
|
-
this.updateQueue = new UpdateQueue(this.
|
|
4064
|
+
this.updateQueue = new UpdateQueue(this.remoteDB, this.remoteDB);
|
|
3551
4065
|
}
|
|
3552
4066
|
getCourseID() {
|
|
3553
4067
|
return this.id;
|
|
@@ -3635,7 +4149,7 @@ var init_courseDB = __esm({
|
|
|
3635
4149
|
};
|
|
3636
4150
|
}
|
|
3637
4151
|
async removeCard(id) {
|
|
3638
|
-
const doc = await this.
|
|
4152
|
+
const doc = await this.remoteDB.get(id);
|
|
3639
4153
|
if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
|
|
3640
4154
|
throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
|
|
3641
4155
|
}
|
|
@@ -3656,7 +4170,7 @@ var init_courseDB = __esm({
|
|
|
3656
4170
|
} catch (error) {
|
|
3657
4171
|
logger.error(`Error removing card ${id} from tags: ${error}`);
|
|
3658
4172
|
}
|
|
3659
|
-
return this.
|
|
4173
|
+
return this.remoteDB.remove(doc);
|
|
3660
4174
|
}
|
|
3661
4175
|
async getCardDisplayableDataIDs(id) {
|
|
3662
4176
|
logger.debug(id.join(", "));
|
|
@@ -3758,8 +4272,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3758
4272
|
if (cardIds.length === 0) {
|
|
3759
4273
|
return /* @__PURE__ */ new Map();
|
|
3760
4274
|
}
|
|
3761
|
-
const
|
|
3762
|
-
const result = await db.query("getTags", {
|
|
4275
|
+
const result = await this.db.query("getTags", {
|
|
3763
4276
|
keys: cardIds,
|
|
3764
4277
|
include_docs: false
|
|
3765
4278
|
});
|
|
@@ -3776,6 +4289,14 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3776
4289
|
}
|
|
3777
4290
|
return tagsByCard;
|
|
3778
4291
|
}
|
|
4292
|
+
async getAllCardIds() {
|
|
4293
|
+
const result = await this.db.allDocs({
|
|
4294
|
+
startkey: "CARD-",
|
|
4295
|
+
endkey: "CARD-\uFFF0",
|
|
4296
|
+
include_docs: false
|
|
4297
|
+
});
|
|
4298
|
+
return result.rows.map((row) => row.id);
|
|
4299
|
+
}
|
|
3779
4300
|
async addTagToCard(cardId, tagId, updateELO) {
|
|
3780
4301
|
return await addTagToCard(
|
|
3781
4302
|
this.id,
|
|
@@ -3842,10 +4363,13 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3842
4363
|
}
|
|
3843
4364
|
}
|
|
3844
4365
|
async getCourseDoc(id, options) {
|
|
3845
|
-
return await
|
|
4366
|
+
return await this.db.get(id, options);
|
|
3846
4367
|
}
|
|
3847
4368
|
async getCourseDocs(ids, options = {}) {
|
|
3848
|
-
return await
|
|
4369
|
+
return await this.db.allDocs({
|
|
4370
|
+
...options,
|
|
4371
|
+
keys: ids
|
|
4372
|
+
});
|
|
3849
4373
|
}
|
|
3850
4374
|
////////////////////////////////////
|
|
3851
4375
|
// NavigationStrategyManager implementation
|
|
@@ -3879,7 +4403,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3879
4403
|
}
|
|
3880
4404
|
async addNavigationStrategy(data) {
|
|
3881
4405
|
logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
|
|
3882
|
-
return this.
|
|
4406
|
+
return this.remoteDB.put(data).then(() => {
|
|
3883
4407
|
});
|
|
3884
4408
|
}
|
|
3885
4409
|
updateNavigationStrategy(id, data) {
|
|
@@ -4295,6 +4819,16 @@ var init_adminDB2 = __esm({
|
|
|
4295
4819
|
}
|
|
4296
4820
|
});
|
|
4297
4821
|
|
|
4822
|
+
// src/impl/couch/CourseSyncService.ts
|
|
4823
|
+
var init_CourseSyncService = __esm({
|
|
4824
|
+
"src/impl/couch/CourseSyncService.ts"() {
|
|
4825
|
+
"use strict";
|
|
4826
|
+
init_pouchdb_setup();
|
|
4827
|
+
init_couch();
|
|
4828
|
+
init_logger();
|
|
4829
|
+
}
|
|
4830
|
+
});
|
|
4831
|
+
|
|
4298
4832
|
// src/impl/couch/auth.ts
|
|
4299
4833
|
var import_cross_fetch;
|
|
4300
4834
|
var init_auth = __esm({
|
|
@@ -4348,15 +4882,6 @@ function getCourseDB2(courseID) {
|
|
|
4348
4882
|
createPouchDBConfig()
|
|
4349
4883
|
);
|
|
4350
4884
|
}
|
|
4351
|
-
function getCourseDocs(courseID, docIDs, options = {}) {
|
|
4352
|
-
return getCourseDB2(courseID).allDocs({
|
|
4353
|
-
...options,
|
|
4354
|
-
keys: docIDs
|
|
4355
|
-
});
|
|
4356
|
-
}
|
|
4357
|
-
function getCourseDoc(courseID, docID, options = {}) {
|
|
4358
|
-
return getCourseDB2(courseID).get(docID, options);
|
|
4359
|
-
}
|
|
4360
4885
|
function filterAllDocsByPrefix2(db, prefix, opts) {
|
|
4361
4886
|
const options = {
|
|
4362
4887
|
startkey: prefix,
|
|
@@ -4390,6 +4915,7 @@ var init_couch = __esm({
|
|
|
4390
4915
|
init_classroomDB2();
|
|
4391
4916
|
init_courseAPI();
|
|
4392
4917
|
init_courseDB();
|
|
4918
|
+
init_CourseSyncService();
|
|
4393
4919
|
init_CouchDBSyncStrategy();
|
|
4394
4920
|
isBrowser = typeof window !== "undefined";
|
|
4395
4921
|
if (isBrowser) {
|
|
@@ -4609,6 +5135,9 @@ Currently logged-in as ${this._username}.`
|
|
|
4609
5135
|
const id = row.id;
|
|
4610
5136
|
return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
|
|
4611
5137
|
id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
|
|
5138
|
+
id.startsWith(DocTypePrefixes["STRATEGY_STATE" /* STRATEGY_STATE */]) || // Strategy state (user prefs, progression)
|
|
5139
|
+
id.startsWith(DocTypePrefixes["USER_OUTCOME" /* USER_OUTCOME */]) || // Evolutionary orchestration outcomes
|
|
5140
|
+
id.startsWith(DocTypePrefixes["STRATEGY_LEARNING_STATE" /* STRATEGY_LEARNING_STATE */]) || // Strategy learning state
|
|
4612
5141
|
id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
4613
5142
|
id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
4614
5143
|
id === _BaseUser.DOC_IDS.CONFIG;
|
|
@@ -6160,6 +6689,7 @@ __export(core_exports, {
|
|
|
6160
6689
|
getDefaultLearnableWeight: () => getDefaultLearnableWeight,
|
|
6161
6690
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
6162
6691
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
6692
|
+
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
6163
6693
|
getStudySource: () => getStudySource,
|
|
6164
6694
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
6165
6695
|
importParsedCards: () => importParsedCards,
|
|
@@ -6224,6 +6754,7 @@ init_core();
|
|
|
6224
6754
|
getDefaultLearnableWeight,
|
|
6225
6755
|
getRegisteredNavigator,
|
|
6226
6756
|
getRegisteredNavigatorNames,
|
|
6757
|
+
getRegisteredNavigatorRole,
|
|
6227
6758
|
getStudySource,
|
|
6228
6759
|
hasRegisteredNavigator,
|
|
6229
6760
|
importParsedCards,
|