@vue-skuilder/db 0.1.31-b → 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-ygoFw9oV.d.ts → contentSource-Bdwkvqa8.d.ts} +16 -0
- package/dist/{contentSource-B7nXusjk.d.cts → contentSource-DF1nUbPQ.d.cts} +16 -0
- package/dist/core/index.d.cts +34 -3
- package/dist/core/index.d.ts +34 -3
- package/dist/core/index.js +510 -50
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +510 -50
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BW7HvkMt.d.cts → dataLayerProvider-BKmVoyJR.d.ts} +20 -1
- package/dist/{dataLayerProvider-BfXUVDuG.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 +730 -41
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +729 -41
- 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 +467 -31
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +467 -31
- 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 +948 -72
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +948 -72
- package/dist/index.mjs.map +1 -1
- 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 +35 -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 +12 -0
- 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 +3 -1
- 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,12 +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";
|
|
816
821
|
init_navigators();
|
|
817
822
|
init_logger();
|
|
823
|
+
_activePipeline = null;
|
|
818
824
|
MAX_RUNS = 10;
|
|
819
825
|
runHistory = [];
|
|
820
826
|
pipelineDebugAPI = {
|
|
@@ -1016,6 +1022,21 @@ var init_PipelineDebugger = __esm({
|
|
|
1016
1022
|
}
|
|
1017
1023
|
console.groupEnd();
|
|
1018
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
|
+
},
|
|
1019
1040
|
/**
|
|
1020
1041
|
* Show help.
|
|
1021
1042
|
*/
|
|
@@ -1028,6 +1049,7 @@ Commands:
|
|
|
1028
1049
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
1029
1050
|
.showCard(cardId) Show provenance trail for a specific card
|
|
1030
1051
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
1052
|
+
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
1031
1053
|
.showRegistry() Show navigator registry (classes + roles)
|
|
1032
1054
|
.showStrategies() Show registry + strategy mapping from last run
|
|
1033
1055
|
.listRuns() List all captured runs in table format
|
|
@@ -1039,7 +1061,7 @@ Commands:
|
|
|
1039
1061
|
Example:
|
|
1040
1062
|
window.skuilder.pipeline.showLastRun()
|
|
1041
1063
|
window.skuilder.pipeline.showRun(1)
|
|
1042
|
-
window.skuilder.pipeline.
|
|
1064
|
+
await window.skuilder.pipeline.diagnoseCardSpace()
|
|
1043
1065
|
`);
|
|
1044
1066
|
}
|
|
1045
1067
|
};
|
|
@@ -1334,6 +1356,69 @@ var init_generators = __esm({
|
|
|
1334
1356
|
}
|
|
1335
1357
|
});
|
|
1336
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
|
+
|
|
1337
1422
|
// src/core/navigators/generators/srs.ts
|
|
1338
1423
|
var srs_exports = {};
|
|
1339
1424
|
__export(srs_exports, {
|
|
@@ -1528,6 +1613,7 @@ var init_ = __esm({
|
|
|
1528
1613
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
1529
1614
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
1530
1615
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
1616
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
1531
1617
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
1532
1618
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports))
|
|
1533
1619
|
});
|
|
@@ -1728,6 +1814,8 @@ var init_hierarchyDefinition = __esm({
|
|
|
1728
1814
|
if (userTagElo.count < minCount) return false;
|
|
1729
1815
|
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1730
1816
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
1817
|
+
} else if (prereq.masteryThreshold?.minCount !== void 0) {
|
|
1818
|
+
return true;
|
|
1731
1819
|
} else {
|
|
1732
1820
|
return userTagElo.score >= userGlobalElo;
|
|
1733
1821
|
}
|
|
@@ -1803,14 +1891,38 @@ var init_hierarchyDefinition = __esm({
|
|
|
1803
1891
|
};
|
|
1804
1892
|
}
|
|
1805
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
|
+
}
|
|
1806
1914
|
/**
|
|
1807
1915
|
* CardFilter.transform implementation.
|
|
1808
1916
|
*
|
|
1809
|
-
*
|
|
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
|
|
1810
1921
|
*/
|
|
1811
1922
|
async transform(cards, context) {
|
|
1812
1923
|
const masteredTags = await this.getMasteredTags(context);
|
|
1813
1924
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
1925
|
+
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
1814
1926
|
const gated = [];
|
|
1815
1927
|
for (const card of cards) {
|
|
1816
1928
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
@@ -1819,9 +1931,27 @@ var init_hierarchyDefinition = __esm({
|
|
|
1819
1931
|
unlockedTags,
|
|
1820
1932
|
masteredTags
|
|
1821
1933
|
);
|
|
1822
|
-
const LOCKED_PENALTY = 0.
|
|
1823
|
-
|
|
1824
|
-
|
|
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
|
+
}
|
|
1825
1955
|
gated.push({
|
|
1826
1956
|
...card,
|
|
1827
1957
|
score: finalScore,
|
|
@@ -1833,7 +1963,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1833
1963
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-hierarchy",
|
|
1834
1964
|
action,
|
|
1835
1965
|
score: finalScore,
|
|
1836
|
-
reason
|
|
1966
|
+
reason: finalReason
|
|
1837
1967
|
}
|
|
1838
1968
|
]
|
|
1839
1969
|
});
|
|
@@ -2767,6 +2897,18 @@ var Pipeline_exports = {};
|
|
|
2767
2897
|
__export(Pipeline_exports, {
|
|
2768
2898
|
Pipeline: () => Pipeline
|
|
2769
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
|
+
}
|
|
2770
2912
|
function logPipelineConfig(generator, filters) {
|
|
2771
2913
|
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
2772
2914
|
logger.info(
|
|
@@ -2801,6 +2943,21 @@ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCo
|
|
|
2801
2943
|
\u{1F4A1} Inspect: window.skuilder.pipeline`
|
|
2802
2944
|
);
|
|
2803
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
|
+
}
|
|
2804
2961
|
function logCardProvenance(cards, maxCards = 3) {
|
|
2805
2962
|
const cardsToLog = cards.slice(0, maxCards);
|
|
2806
2963
|
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
@@ -2815,7 +2972,7 @@ function logCardProvenance(cards, maxCards = 3) {
|
|
|
2815
2972
|
}
|
|
2816
2973
|
}
|
|
2817
2974
|
}
|
|
2818
|
-
var import_common8, Pipeline;
|
|
2975
|
+
var import_common8, VERBOSE_RESULTS, Pipeline;
|
|
2819
2976
|
var init_Pipeline = __esm({
|
|
2820
2977
|
"src/core/navigators/Pipeline.ts"() {
|
|
2821
2978
|
"use strict";
|
|
@@ -2824,9 +2981,31 @@ var init_Pipeline = __esm({
|
|
|
2824
2981
|
init_logger();
|
|
2825
2982
|
init_orchestration();
|
|
2826
2983
|
init_PipelineDebugger();
|
|
2984
|
+
VERBOSE_RESULTS = true;
|
|
2827
2985
|
Pipeline = class extends ContentNavigator {
|
|
2828
2986
|
generator;
|
|
2829
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;
|
|
2830
3009
|
/**
|
|
2831
3010
|
* Create a new pipeline.
|
|
2832
3011
|
*
|
|
@@ -2847,6 +3026,17 @@ var init_Pipeline = __esm({
|
|
|
2847
3026
|
logger.error(`[pipeline] Failed to lookup courseCfg: ${e}`);
|
|
2848
3027
|
});
|
|
2849
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)}`);
|
|
2850
3040
|
}
|
|
2851
3041
|
/**
|
|
2852
3042
|
* Get weighted cards by running generator and applying filters.
|
|
@@ -2863,13 +3053,15 @@ var init_Pipeline = __esm({
|
|
|
2863
3053
|
* @returns Cards sorted by score descending
|
|
2864
3054
|
*/
|
|
2865
3055
|
async getWeightedCards(limit) {
|
|
3056
|
+
const t0 = performance.now();
|
|
2866
3057
|
const context = await this.buildContext();
|
|
2867
|
-
const
|
|
2868
|
-
const fetchLimit =
|
|
3058
|
+
const tContext = performance.now();
|
|
3059
|
+
const fetchLimit = 500;
|
|
2869
3060
|
logger.debug(
|
|
2870
3061
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
2871
3062
|
);
|
|
2872
3063
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
3064
|
+
const tGenerate = performance.now();
|
|
2873
3065
|
const generatedCount = cards.length;
|
|
2874
3066
|
let generatorSummaries;
|
|
2875
3067
|
if (this.generator.generators) {
|
|
@@ -2898,6 +3090,7 @@ var init_Pipeline = __esm({
|
|
|
2898
3090
|
}
|
|
2899
3091
|
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
2900
3092
|
cards = await this.hydrateTags(cards);
|
|
3093
|
+
const tHydrate = performance.now();
|
|
2901
3094
|
const allCardsBeforeFiltering = [...cards];
|
|
2902
3095
|
const filterImpacts = [];
|
|
2903
3096
|
for (const filter of this.filters) {
|
|
@@ -2916,8 +3109,17 @@ var init_Pipeline = __esm({
|
|
|
2916
3109
|
logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
|
|
2917
3110
|
}
|
|
2918
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
|
+
}
|
|
2919
3117
|
cards.sort((a, b) => b.score - a.score);
|
|
3118
|
+
const tFilter = performance.now();
|
|
2920
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
|
+
);
|
|
2921
3123
|
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
2922
3124
|
logExecutionSummary(
|
|
2923
3125
|
this.generator.name,
|
|
@@ -2927,6 +3129,7 @@ var init_Pipeline = __esm({
|
|
|
2927
3129
|
topScores,
|
|
2928
3130
|
filterImpacts
|
|
2929
3131
|
);
|
|
3132
|
+
logResultCards(result);
|
|
2930
3133
|
logCardProvenance(result, 3);
|
|
2931
3134
|
try {
|
|
2932
3135
|
const courseName = await this.course?.getCourseConfig().then((c) => c.name).catch(() => void 0);
|
|
@@ -2953,6 +3156,10 @@ var init_Pipeline = __esm({
|
|
|
2953
3156
|
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
2954
3157
|
* making individual getAppliedTags() calls.
|
|
2955
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
|
+
*
|
|
2956
3163
|
* @param cards - Cards to hydrate
|
|
2957
3164
|
* @returns Cards with tags populated
|
|
2958
3165
|
*/
|
|
@@ -2960,14 +3167,128 @@ var init_Pipeline = __esm({
|
|
|
2960
3167
|
if (cards.length === 0) {
|
|
2961
3168
|
return cards;
|
|
2962
3169
|
}
|
|
2963
|
-
const
|
|
2964
|
-
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
|
+
}
|
|
2965
3186
|
logTagHydration(cards, tagsByCard);
|
|
2966
3187
|
return cards.map((card) => ({
|
|
2967
3188
|
...card,
|
|
2968
|
-
tags:
|
|
3189
|
+
tags: this._tagCache.get(card.cardId) ?? []
|
|
2969
3190
|
}));
|
|
2970
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
|
+
}
|
|
2971
3292
|
/**
|
|
2972
3293
|
* Build shared context for generator and filters.
|
|
2973
3294
|
*
|
|
@@ -2985,7 +3306,10 @@ var init_Pipeline = __esm({
|
|
|
2985
3306
|
} catch (e) {
|
|
2986
3307
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
2987
3308
|
}
|
|
2988
|
-
|
|
3309
|
+
if (!this._cachedOrchestration) {
|
|
3310
|
+
this._cachedOrchestration = await createOrchestrationContext(this.user, this.course);
|
|
3311
|
+
}
|
|
3312
|
+
const orchestration = this._cachedOrchestration;
|
|
2989
3313
|
return {
|
|
2990
3314
|
user: this.user,
|
|
2991
3315
|
course: this.course,
|
|
@@ -3029,6 +3353,87 @@ var init_Pipeline = __esm({
|
|
|
3029
3353
|
}
|
|
3030
3354
|
return [...new Set(ids)];
|
|
3031
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
|
+
}
|
|
3032
3437
|
};
|
|
3033
3438
|
}
|
|
3034
3439
|
});
|
|
@@ -3133,23 +3538,25 @@ var init_PipelineAssembler = __esm({
|
|
|
3133
3538
|
warnings.push(`Unknown strategy type '${s.implementingClass}', skipping: ${s.name}`);
|
|
3134
3539
|
}
|
|
3135
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
|
+
}
|
|
3136
3552
|
if (generatorStrategies.length === 0) {
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
3141
|
-
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
} else {
|
|
3145
|
-
warnings.push("No generator strategy found");
|
|
3146
|
-
return {
|
|
3147
|
-
pipeline: null,
|
|
3148
|
-
generatorStrategies: [],
|
|
3149
|
-
filterStrategies: [],
|
|
3150
|
-
warnings
|
|
3151
|
-
};
|
|
3152
|
-
}
|
|
3553
|
+
warnings.push("No generator strategy found");
|
|
3554
|
+
return {
|
|
3555
|
+
pipeline: null,
|
|
3556
|
+
generatorStrategies: [],
|
|
3557
|
+
filterStrategies: [],
|
|
3558
|
+
warnings
|
|
3559
|
+
};
|
|
3153
3560
|
}
|
|
3154
3561
|
let generator;
|
|
3155
3562
|
if (generatorStrategies.length === 1) {
|
|
@@ -3227,6 +3634,7 @@ var init_3 = __esm({
|
|
|
3227
3634
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
3228
3635
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
3229
3636
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
3637
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
3230
3638
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
3231
3639
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
3232
3640
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
@@ -3275,8 +3683,10 @@ async function initializeNavigatorRegistry() {
|
|
|
3275
3683
|
Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
3276
3684
|
Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
3277
3685
|
]);
|
|
3686
|
+
const prescribedModule = await Promise.resolve().then(() => (init_prescribed(), prescribed_exports));
|
|
3278
3687
|
registerNavigator("elo", eloModule.default);
|
|
3279
3688
|
registerNavigator("srs", srsModule.default);
|
|
3689
|
+
registerNavigator("prescribed", prescribedModule.default);
|
|
3280
3690
|
const [
|
|
3281
3691
|
hierarchyModule,
|
|
3282
3692
|
interferenceModule,
|
|
@@ -3331,6 +3741,7 @@ var init_navigators = __esm({
|
|
|
3331
3741
|
Navigators = /* @__PURE__ */ ((Navigators2) => {
|
|
3332
3742
|
Navigators2["ELO"] = "elo";
|
|
3333
3743
|
Navigators2["SRS"] = "srs";
|
|
3744
|
+
Navigators2["PRESCRIBED"] = "prescribed";
|
|
3334
3745
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
3335
3746
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
3336
3747
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
@@ -3345,6 +3756,7 @@ var init_navigators = __esm({
|
|
|
3345
3756
|
NavigatorRoles = {
|
|
3346
3757
|
["elo" /* ELO */]: "generator" /* GENERATOR */,
|
|
3347
3758
|
["srs" /* SRS */]: "generator" /* GENERATOR */,
|
|
3759
|
+
["prescribed" /* PRESCRIBED */]: "generator" /* GENERATOR */,
|
|
3348
3760
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
3349
3761
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
3350
3762
|
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
@@ -3509,6 +3921,12 @@ var init_navigators = __esm({
|
|
|
3509
3921
|
async getWeightedCards(_limit) {
|
|
3510
3922
|
throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
|
|
3511
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
|
+
}
|
|
3512
3930
|
};
|
|
3513
3931
|
}
|
|
3514
3932
|
});
|
|
@@ -3608,15 +4026,42 @@ var init_courseDB = __esm({
|
|
|
3608
4026
|
// private log(msg: string): void {
|
|
3609
4027
|
// log(`CourseLog: ${this.id}\n ${msg}`);
|
|
3610
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
|
+
*/
|
|
3611
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;
|
|
3612
4046
|
id;
|
|
3613
4047
|
_getCurrentUser;
|
|
3614
4048
|
updateQueue;
|
|
3615
|
-
|
|
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) {
|
|
3616
4059
|
this.id = id;
|
|
3617
|
-
|
|
4060
|
+
const remote = getCourseDB2(this.id);
|
|
4061
|
+
this.remoteDB = remote;
|
|
4062
|
+
this.db = localDB ?? remote;
|
|
3618
4063
|
this._getCurrentUser = userLookup;
|
|
3619
|
-
this.updateQueue = new UpdateQueue(this.
|
|
4064
|
+
this.updateQueue = new UpdateQueue(this.remoteDB, this.remoteDB);
|
|
3620
4065
|
}
|
|
3621
4066
|
getCourseID() {
|
|
3622
4067
|
return this.id;
|
|
@@ -3704,7 +4149,7 @@ var init_courseDB = __esm({
|
|
|
3704
4149
|
};
|
|
3705
4150
|
}
|
|
3706
4151
|
async removeCard(id) {
|
|
3707
|
-
const doc = await this.
|
|
4152
|
+
const doc = await this.remoteDB.get(id);
|
|
3708
4153
|
if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
|
|
3709
4154
|
throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
|
|
3710
4155
|
}
|
|
@@ -3725,7 +4170,7 @@ var init_courseDB = __esm({
|
|
|
3725
4170
|
} catch (error) {
|
|
3726
4171
|
logger.error(`Error removing card ${id} from tags: ${error}`);
|
|
3727
4172
|
}
|
|
3728
|
-
return this.
|
|
4173
|
+
return this.remoteDB.remove(doc);
|
|
3729
4174
|
}
|
|
3730
4175
|
async getCardDisplayableDataIDs(id) {
|
|
3731
4176
|
logger.debug(id.join(", "));
|
|
@@ -3827,8 +4272,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3827
4272
|
if (cardIds.length === 0) {
|
|
3828
4273
|
return /* @__PURE__ */ new Map();
|
|
3829
4274
|
}
|
|
3830
|
-
const
|
|
3831
|
-
const result = await db.query("getTags", {
|
|
4275
|
+
const result = await this.db.query("getTags", {
|
|
3832
4276
|
keys: cardIds,
|
|
3833
4277
|
include_docs: false
|
|
3834
4278
|
});
|
|
@@ -3845,6 +4289,14 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3845
4289
|
}
|
|
3846
4290
|
return tagsByCard;
|
|
3847
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
|
+
}
|
|
3848
4300
|
async addTagToCard(cardId, tagId, updateELO) {
|
|
3849
4301
|
return await addTagToCard(
|
|
3850
4302
|
this.id,
|
|
@@ -3911,10 +4363,13 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3911
4363
|
}
|
|
3912
4364
|
}
|
|
3913
4365
|
async getCourseDoc(id, options) {
|
|
3914
|
-
return await
|
|
4366
|
+
return await this.db.get(id, options);
|
|
3915
4367
|
}
|
|
3916
4368
|
async getCourseDocs(ids, options = {}) {
|
|
3917
|
-
return await
|
|
4369
|
+
return await this.db.allDocs({
|
|
4370
|
+
...options,
|
|
4371
|
+
keys: ids
|
|
4372
|
+
});
|
|
3918
4373
|
}
|
|
3919
4374
|
////////////////////////////////////
|
|
3920
4375
|
// NavigationStrategyManager implementation
|
|
@@ -3948,7 +4403,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3948
4403
|
}
|
|
3949
4404
|
async addNavigationStrategy(data) {
|
|
3950
4405
|
logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
|
|
3951
|
-
return this.
|
|
4406
|
+
return this.remoteDB.put(data).then(() => {
|
|
3952
4407
|
});
|
|
3953
4408
|
}
|
|
3954
4409
|
updateNavigationStrategy(id, data) {
|
|
@@ -4364,6 +4819,16 @@ var init_adminDB2 = __esm({
|
|
|
4364
4819
|
}
|
|
4365
4820
|
});
|
|
4366
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
|
+
|
|
4367
4832
|
// src/impl/couch/auth.ts
|
|
4368
4833
|
var import_cross_fetch;
|
|
4369
4834
|
var init_auth = __esm({
|
|
@@ -4417,15 +4882,6 @@ function getCourseDB2(courseID) {
|
|
|
4417
4882
|
createPouchDBConfig()
|
|
4418
4883
|
);
|
|
4419
4884
|
}
|
|
4420
|
-
function getCourseDocs(courseID, docIDs, options = {}) {
|
|
4421
|
-
return getCourseDB2(courseID).allDocs({
|
|
4422
|
-
...options,
|
|
4423
|
-
keys: docIDs
|
|
4424
|
-
});
|
|
4425
|
-
}
|
|
4426
|
-
function getCourseDoc(courseID, docID, options = {}) {
|
|
4427
|
-
return getCourseDB2(courseID).get(docID, options);
|
|
4428
|
-
}
|
|
4429
4885
|
function filterAllDocsByPrefix2(db, prefix, opts) {
|
|
4430
4886
|
const options = {
|
|
4431
4887
|
startkey: prefix,
|
|
@@ -4459,6 +4915,7 @@ var init_couch = __esm({
|
|
|
4459
4915
|
init_classroomDB2();
|
|
4460
4916
|
init_courseAPI();
|
|
4461
4917
|
init_courseDB();
|
|
4918
|
+
init_CourseSyncService();
|
|
4462
4919
|
init_CouchDBSyncStrategy();
|
|
4463
4920
|
isBrowser = typeof window !== "undefined";
|
|
4464
4921
|
if (isBrowser) {
|
|
@@ -4678,6 +5135,9 @@ Currently logged-in as ${this._username}.`
|
|
|
4678
5135
|
const id = row.id;
|
|
4679
5136
|
return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
|
|
4680
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
|
|
4681
5141
|
id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
4682
5142
|
id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
4683
5143
|
id === _BaseUser.DOC_IDS.CONFIG;
|