@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.mjs
CHANGED
|
@@ -702,8 +702,12 @@ __export(PipelineDebugger_exports, {
|
|
|
702
702
|
buildRunReport: () => buildRunReport,
|
|
703
703
|
captureRun: () => captureRun,
|
|
704
704
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
705
|
-
pipelineDebugAPI: () => pipelineDebugAPI
|
|
705
|
+
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
706
|
+
registerPipelineForDebug: () => registerPipelineForDebug
|
|
706
707
|
});
|
|
708
|
+
function registerPipelineForDebug(pipeline) {
|
|
709
|
+
_activePipeline = pipeline;
|
|
710
|
+
}
|
|
707
711
|
function getOrigin(card) {
|
|
708
712
|
const firstEntry = card.provenance[0];
|
|
709
713
|
if (!firstEntry) return "unknown";
|
|
@@ -731,6 +735,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
731
735
|
origin: getOrigin(card),
|
|
732
736
|
finalScore: card.score,
|
|
733
737
|
provenance: card.provenance,
|
|
738
|
+
tags: card.tags,
|
|
734
739
|
selected: selectedIds.has(card.cardId)
|
|
735
740
|
}));
|
|
736
741
|
const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
|
|
@@ -786,12 +791,13 @@ function mountPipelineDebugger() {
|
|
|
786
791
|
win.skuilder = win.skuilder || {};
|
|
787
792
|
win.skuilder.pipeline = pipelineDebugAPI;
|
|
788
793
|
}
|
|
789
|
-
var MAX_RUNS, runHistory, pipelineDebugAPI;
|
|
794
|
+
var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
|
|
790
795
|
var init_PipelineDebugger = __esm({
|
|
791
796
|
"src/core/navigators/PipelineDebugger.ts"() {
|
|
792
797
|
"use strict";
|
|
793
798
|
init_navigators();
|
|
794
799
|
init_logger();
|
|
800
|
+
_activePipeline = null;
|
|
795
801
|
MAX_RUNS = 10;
|
|
796
802
|
runHistory = [];
|
|
797
803
|
pipelineDebugAPI = {
|
|
@@ -993,6 +999,21 @@ var init_PipelineDebugger = __esm({
|
|
|
993
999
|
}
|
|
994
1000
|
console.groupEnd();
|
|
995
1001
|
},
|
|
1002
|
+
/**
|
|
1003
|
+
* Scan the full card space through the filter chain for the current user.
|
|
1004
|
+
*
|
|
1005
|
+
* Reports how many cards are well-indicated and how many are new.
|
|
1006
|
+
* Use this to understand how the search space grows during onboarding.
|
|
1007
|
+
*
|
|
1008
|
+
* @param threshold - Score threshold for "well indicated" (default 0.10)
|
|
1009
|
+
*/
|
|
1010
|
+
async diagnoseCardSpace(threshold) {
|
|
1011
|
+
if (!_activePipeline) {
|
|
1012
|
+
logger.info("[Pipeline Debug] No active pipeline. Run a session first.");
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
return _activePipeline.diagnoseCardSpace({ threshold });
|
|
1016
|
+
},
|
|
996
1017
|
/**
|
|
997
1018
|
* Show help.
|
|
998
1019
|
*/
|
|
@@ -1005,6 +1026,7 @@ Commands:
|
|
|
1005
1026
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
1006
1027
|
.showCard(cardId) Show provenance trail for a specific card
|
|
1007
1028
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
1029
|
+
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
1008
1030
|
.showRegistry() Show navigator registry (classes + roles)
|
|
1009
1031
|
.showStrategies() Show registry + strategy mapping from last run
|
|
1010
1032
|
.listRuns() List all captured runs in table format
|
|
@@ -1016,7 +1038,7 @@ Commands:
|
|
|
1016
1038
|
Example:
|
|
1017
1039
|
window.skuilder.pipeline.showLastRun()
|
|
1018
1040
|
window.skuilder.pipeline.showRun(1)
|
|
1019
|
-
window.skuilder.pipeline.
|
|
1041
|
+
await window.skuilder.pipeline.diagnoseCardSpace()
|
|
1020
1042
|
`);
|
|
1021
1043
|
}
|
|
1022
1044
|
};
|
|
@@ -1311,6 +1333,69 @@ var init_generators = __esm({
|
|
|
1311
1333
|
}
|
|
1312
1334
|
});
|
|
1313
1335
|
|
|
1336
|
+
// src/core/navigators/generators/prescribed.ts
|
|
1337
|
+
var prescribed_exports = {};
|
|
1338
|
+
__export(prescribed_exports, {
|
|
1339
|
+
default: () => PrescribedCardsGenerator
|
|
1340
|
+
});
|
|
1341
|
+
var PrescribedCardsGenerator;
|
|
1342
|
+
var init_prescribed = __esm({
|
|
1343
|
+
"src/core/navigators/generators/prescribed.ts"() {
|
|
1344
|
+
"use strict";
|
|
1345
|
+
init_navigators();
|
|
1346
|
+
init_logger();
|
|
1347
|
+
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1348
|
+
name;
|
|
1349
|
+
config;
|
|
1350
|
+
constructor(user, course, strategyData) {
|
|
1351
|
+
super(user, course, strategyData);
|
|
1352
|
+
this.name = strategyData.name || "Prescribed Cards";
|
|
1353
|
+
try {
|
|
1354
|
+
const parsed = JSON.parse(strategyData.serializedData);
|
|
1355
|
+
this.config = { cardIds: parsed.cardIds || [] };
|
|
1356
|
+
} catch {
|
|
1357
|
+
this.config = { cardIds: [] };
|
|
1358
|
+
}
|
|
1359
|
+
logger.debug(
|
|
1360
|
+
`[Prescribed] Initialized with ${this.config.cardIds.length} prescribed cards`
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
async getWeightedCards(limit, _context) {
|
|
1364
|
+
if (this.config.cardIds.length === 0) {
|
|
1365
|
+
return [];
|
|
1366
|
+
}
|
|
1367
|
+
const courseId = this.course.getCourseID();
|
|
1368
|
+
const activeCards = await this.user.getActiveCards();
|
|
1369
|
+
const activeIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
1370
|
+
const eligibleIds = this.config.cardIds.filter((id) => !activeIds.has(id));
|
|
1371
|
+
if (eligibleIds.length === 0) {
|
|
1372
|
+
logger.debug("[Prescribed] All prescribed cards already active, returning empty");
|
|
1373
|
+
return [];
|
|
1374
|
+
}
|
|
1375
|
+
const cards = eligibleIds.slice(0, limit).map((cardId) => ({
|
|
1376
|
+
cardId,
|
|
1377
|
+
courseId,
|
|
1378
|
+
score: 1,
|
|
1379
|
+
provenance: [
|
|
1380
|
+
{
|
|
1381
|
+
strategy: "prescribed",
|
|
1382
|
+
strategyName: this.strategyName || this.name,
|
|
1383
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1384
|
+
action: "generated",
|
|
1385
|
+
score: 1,
|
|
1386
|
+
reason: `Prescribed card (${eligibleIds.length} eligible of ${this.config.cardIds.length} configured)`
|
|
1387
|
+
}
|
|
1388
|
+
]
|
|
1389
|
+
}));
|
|
1390
|
+
logger.info(
|
|
1391
|
+
`[Prescribed] Emitting ${cards.length} cards (${eligibleIds.length} eligible, ${activeIds.size} already active)`
|
|
1392
|
+
);
|
|
1393
|
+
return cards;
|
|
1394
|
+
}
|
|
1395
|
+
};
|
|
1396
|
+
}
|
|
1397
|
+
});
|
|
1398
|
+
|
|
1314
1399
|
// src/core/navigators/generators/srs.ts
|
|
1315
1400
|
var srs_exports = {};
|
|
1316
1401
|
__export(srs_exports, {
|
|
@@ -1505,6 +1590,7 @@ var init_ = __esm({
|
|
|
1505
1590
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
1506
1591
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
1507
1592
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
1593
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
1508
1594
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
1509
1595
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports))
|
|
1510
1596
|
});
|
|
@@ -1705,6 +1791,8 @@ var init_hierarchyDefinition = __esm({
|
|
|
1705
1791
|
if (userTagElo.count < minCount) return false;
|
|
1706
1792
|
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1707
1793
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
1794
|
+
} else if (prereq.masteryThreshold?.minCount !== void 0) {
|
|
1795
|
+
return true;
|
|
1708
1796
|
} else {
|
|
1709
1797
|
return userTagElo.score >= userGlobalElo;
|
|
1710
1798
|
}
|
|
@@ -1780,14 +1868,38 @@ var init_hierarchyDefinition = __esm({
|
|
|
1780
1868
|
};
|
|
1781
1869
|
}
|
|
1782
1870
|
}
|
|
1871
|
+
/**
|
|
1872
|
+
* Build a map of prereq tag → max configured boost for all *closed* gates.
|
|
1873
|
+
*
|
|
1874
|
+
* When a gate is closed (prereqs unmet), cards carrying that gate's prereq
|
|
1875
|
+
* tags get boosted — steering the pipeline toward content that helps unlock
|
|
1876
|
+
* the gated material. Once the gate opens, the boost disappears.
|
|
1877
|
+
*/
|
|
1878
|
+
getPreReqBoosts(unlockedTags, masteredTags) {
|
|
1879
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
1880
|
+
for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
|
|
1881
|
+
if (unlockedTags.has(tagId)) continue;
|
|
1882
|
+
for (const prereq of prereqs) {
|
|
1883
|
+
if (!prereq.preReqBoost || prereq.preReqBoost <= 1) continue;
|
|
1884
|
+
if (masteredTags.has(prereq.tag)) continue;
|
|
1885
|
+
const existing = boosts.get(prereq.tag) ?? 1;
|
|
1886
|
+
boosts.set(prereq.tag, Math.max(existing, prereq.preReqBoost));
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
return boosts;
|
|
1890
|
+
}
|
|
1783
1891
|
/**
|
|
1784
1892
|
* CardFilter.transform implementation.
|
|
1785
1893
|
*
|
|
1786
|
-
*
|
|
1894
|
+
* Two effects:
|
|
1895
|
+
* 1. Cards with locked tags receive score * 0.05 (gating penalty)
|
|
1896
|
+
* 2. Cards carrying prereq tags of closed gates receive a configured
|
|
1897
|
+
* boost (preReqBoost), steering toward content that unlocks gates
|
|
1787
1898
|
*/
|
|
1788
1899
|
async transform(cards, context) {
|
|
1789
1900
|
const masteredTags = await this.getMasteredTags(context);
|
|
1790
1901
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
1902
|
+
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
1791
1903
|
const gated = [];
|
|
1792
1904
|
for (const card of cards) {
|
|
1793
1905
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
@@ -1796,9 +1908,27 @@ var init_hierarchyDefinition = __esm({
|
|
|
1796
1908
|
unlockedTags,
|
|
1797
1909
|
masteredTags
|
|
1798
1910
|
);
|
|
1799
|
-
const LOCKED_PENALTY = 0.
|
|
1800
|
-
|
|
1801
|
-
|
|
1911
|
+
const LOCKED_PENALTY = 0.02;
|
|
1912
|
+
let finalScore = isUnlocked ? card.score : card.score * LOCKED_PENALTY;
|
|
1913
|
+
let action = isUnlocked ? "passed" : "penalized";
|
|
1914
|
+
let finalReason = reason;
|
|
1915
|
+
if (isUnlocked && preReqBoosts.size > 0) {
|
|
1916
|
+
const cardTags = card.tags ?? [];
|
|
1917
|
+
let maxBoost = 1;
|
|
1918
|
+
const boostedPrereqs = [];
|
|
1919
|
+
for (const tag of cardTags) {
|
|
1920
|
+
const boost = preReqBoosts.get(tag);
|
|
1921
|
+
if (boost && boost > maxBoost) {
|
|
1922
|
+
maxBoost = boost;
|
|
1923
|
+
boostedPrereqs.push(tag);
|
|
1924
|
+
}
|
|
1925
|
+
}
|
|
1926
|
+
if (maxBoost > 1) {
|
|
1927
|
+
finalScore *= maxBoost;
|
|
1928
|
+
action = "boosted";
|
|
1929
|
+
finalReason = `${reason} | preReqBoost \xD7${maxBoost.toFixed(2)} for ${boostedPrereqs.join(", ")}`;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1802
1932
|
gated.push({
|
|
1803
1933
|
...card,
|
|
1804
1934
|
score: finalScore,
|
|
@@ -1810,7 +1940,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1810
1940
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-hierarchy",
|
|
1811
1941
|
action,
|
|
1812
1942
|
score: finalScore,
|
|
1813
|
-
reason
|
|
1943
|
+
reason: finalReason
|
|
1814
1944
|
}
|
|
1815
1945
|
]
|
|
1816
1946
|
});
|
|
@@ -2745,6 +2875,18 @@ __export(Pipeline_exports, {
|
|
|
2745
2875
|
Pipeline: () => Pipeline
|
|
2746
2876
|
});
|
|
2747
2877
|
import { toCourseElo as toCourseElo5 } from "@vue-skuilder/common";
|
|
2878
|
+
function globToRegex(pattern) {
|
|
2879
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
2880
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
2881
|
+
return new RegExp(`^${withWildcards}$`);
|
|
2882
|
+
}
|
|
2883
|
+
function globMatch(value, pattern) {
|
|
2884
|
+
if (!pattern.includes("*")) return value === pattern;
|
|
2885
|
+
return globToRegex(pattern).test(value);
|
|
2886
|
+
}
|
|
2887
|
+
function cardMatchesTagPattern(card, pattern) {
|
|
2888
|
+
return (card.tags ?? []).some((tag) => globMatch(tag, pattern));
|
|
2889
|
+
}
|
|
2748
2890
|
function logPipelineConfig(generator, filters) {
|
|
2749
2891
|
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
2750
2892
|
logger.info(
|
|
@@ -2779,6 +2921,21 @@ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCo
|
|
|
2779
2921
|
\u{1F4A1} Inspect: window.skuilder.pipeline`
|
|
2780
2922
|
);
|
|
2781
2923
|
}
|
|
2924
|
+
function logResultCards(cards) {
|
|
2925
|
+
if (!VERBOSE_RESULTS || cards.length === 0) return;
|
|
2926
|
+
logger.info(`[Pipeline] Results (${cards.length} cards):`);
|
|
2927
|
+
for (let i = 0; i < cards.length; i++) {
|
|
2928
|
+
const c = cards[i];
|
|
2929
|
+
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
2930
|
+
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
2931
|
+
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
2932
|
+
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
2933
|
+
}).join(" | ");
|
|
2934
|
+
logger.info(
|
|
2935
|
+
`[Pipeline] ${String(i + 1).padStart(2)}. ${c.score.toFixed(4)} ${c.cardId} [${tags}]${filters ? ` {${filters}}` : ""}`
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
}
|
|
2782
2939
|
function logCardProvenance(cards, maxCards = 3) {
|
|
2783
2940
|
const cardsToLog = cards.slice(0, maxCards);
|
|
2784
2941
|
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
@@ -2793,7 +2950,7 @@ function logCardProvenance(cards, maxCards = 3) {
|
|
|
2793
2950
|
}
|
|
2794
2951
|
}
|
|
2795
2952
|
}
|
|
2796
|
-
var Pipeline;
|
|
2953
|
+
var VERBOSE_RESULTS, Pipeline;
|
|
2797
2954
|
var init_Pipeline = __esm({
|
|
2798
2955
|
"src/core/navigators/Pipeline.ts"() {
|
|
2799
2956
|
"use strict";
|
|
@@ -2801,9 +2958,31 @@ var init_Pipeline = __esm({
|
|
|
2801
2958
|
init_logger();
|
|
2802
2959
|
init_orchestration();
|
|
2803
2960
|
init_PipelineDebugger();
|
|
2961
|
+
VERBOSE_RESULTS = true;
|
|
2804
2962
|
Pipeline = class extends ContentNavigator {
|
|
2805
2963
|
generator;
|
|
2806
2964
|
filters;
|
|
2965
|
+
/**
|
|
2966
|
+
* Cached orchestration context. Course config and salt don't change within
|
|
2967
|
+
* a page load, so we build the orchestration context once and reuse it on
|
|
2968
|
+
* subsequent getWeightedCards() calls (e.g. mid-session replans).
|
|
2969
|
+
*
|
|
2970
|
+
* This eliminates a remote getCourseConfig() round trip per pipeline run.
|
|
2971
|
+
*/
|
|
2972
|
+
_cachedOrchestration = null;
|
|
2973
|
+
/**
|
|
2974
|
+
* Persistent tag cache. Maps cardId → tag names.
|
|
2975
|
+
*
|
|
2976
|
+
* Tags are static within a session (they're set at card generation time),
|
|
2977
|
+
* so we cache them across pipeline runs. On replans, many of the same cards
|
|
2978
|
+
* reappear — cache hits avoid redundant remote getAppliedTagsBatch() queries.
|
|
2979
|
+
*/
|
|
2980
|
+
_tagCache = /* @__PURE__ */ new Map();
|
|
2981
|
+
/**
|
|
2982
|
+
* One-shot replan hints. Applied after the filter chain on the next
|
|
2983
|
+
* getWeightedCards() call, then cleared.
|
|
2984
|
+
*/
|
|
2985
|
+
_ephemeralHints = null;
|
|
2807
2986
|
/**
|
|
2808
2987
|
* Create a new pipeline.
|
|
2809
2988
|
*
|
|
@@ -2824,6 +3003,17 @@ var init_Pipeline = __esm({
|
|
|
2824
3003
|
logger.error(`[pipeline] Failed to lookup courseCfg: ${e}`);
|
|
2825
3004
|
});
|
|
2826
3005
|
logPipelineConfig(generator, filters);
|
|
3006
|
+
registerPipelineForDebug(this);
|
|
3007
|
+
}
|
|
3008
|
+
/**
|
|
3009
|
+
* Set one-shot hints for the next pipeline run.
|
|
3010
|
+
* Consumed after one getWeightedCards() call, then cleared.
|
|
3011
|
+
*
|
|
3012
|
+
* Overrides ContentNavigator.setEphemeralHints() no-op.
|
|
3013
|
+
*/
|
|
3014
|
+
setEphemeralHints(hints) {
|
|
3015
|
+
this._ephemeralHints = hints;
|
|
3016
|
+
logger.info(`[Pipeline] Ephemeral hints set: ${JSON.stringify(hints)}`);
|
|
2827
3017
|
}
|
|
2828
3018
|
/**
|
|
2829
3019
|
* Get weighted cards by running generator and applying filters.
|
|
@@ -2840,13 +3030,15 @@ var init_Pipeline = __esm({
|
|
|
2840
3030
|
* @returns Cards sorted by score descending
|
|
2841
3031
|
*/
|
|
2842
3032
|
async getWeightedCards(limit) {
|
|
3033
|
+
const t0 = performance.now();
|
|
2843
3034
|
const context = await this.buildContext();
|
|
2844
|
-
const
|
|
2845
|
-
const fetchLimit =
|
|
3035
|
+
const tContext = performance.now();
|
|
3036
|
+
const fetchLimit = 500;
|
|
2846
3037
|
logger.debug(
|
|
2847
3038
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
2848
3039
|
);
|
|
2849
3040
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
3041
|
+
const tGenerate = performance.now();
|
|
2850
3042
|
const generatedCount = cards.length;
|
|
2851
3043
|
let generatorSummaries;
|
|
2852
3044
|
if (this.generator.generators) {
|
|
@@ -2875,6 +3067,7 @@ var init_Pipeline = __esm({
|
|
|
2875
3067
|
}
|
|
2876
3068
|
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
2877
3069
|
cards = await this.hydrateTags(cards);
|
|
3070
|
+
const tHydrate = performance.now();
|
|
2878
3071
|
const allCardsBeforeFiltering = [...cards];
|
|
2879
3072
|
const filterImpacts = [];
|
|
2880
3073
|
for (const filter of this.filters) {
|
|
@@ -2893,8 +3086,17 @@ var init_Pipeline = __esm({
|
|
|
2893
3086
|
logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
|
|
2894
3087
|
}
|
|
2895
3088
|
cards = cards.filter((c) => c.score > 0);
|
|
3089
|
+
const hints = this._ephemeralHints;
|
|
3090
|
+
if (hints) {
|
|
3091
|
+
this._ephemeralHints = null;
|
|
3092
|
+
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
3093
|
+
}
|
|
2896
3094
|
cards.sort((a, b) => b.score - a.score);
|
|
3095
|
+
const tFilter = performance.now();
|
|
2897
3096
|
const result = cards.slice(0, limit);
|
|
3097
|
+
logger.info(
|
|
3098
|
+
`[Pipeline:timing] total=${(tFilter - t0).toFixed(0)}ms (context=${(tContext - t0).toFixed(0)} generate=${(tGenerate - tContext).toFixed(0)} hydrate=${(tHydrate - tGenerate).toFixed(0)} filter=${(tFilter - tHydrate).toFixed(0)})`
|
|
3099
|
+
);
|
|
2898
3100
|
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
2899
3101
|
logExecutionSummary(
|
|
2900
3102
|
this.generator.name,
|
|
@@ -2904,6 +3106,7 @@ var init_Pipeline = __esm({
|
|
|
2904
3106
|
topScores,
|
|
2905
3107
|
filterImpacts
|
|
2906
3108
|
);
|
|
3109
|
+
logResultCards(result);
|
|
2907
3110
|
logCardProvenance(result, 3);
|
|
2908
3111
|
try {
|
|
2909
3112
|
const courseName = await this.course?.getCourseConfig().then((c) => c.name).catch(() => void 0);
|
|
@@ -2930,6 +3133,10 @@ var init_Pipeline = __esm({
|
|
|
2930
3133
|
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
2931
3134
|
* making individual getAppliedTags() calls.
|
|
2932
3135
|
*
|
|
3136
|
+
* Uses a persistent tag cache across pipeline runs — tags are static within
|
|
3137
|
+
* a session, so cards seen in a prior run (e.g. before a replan) don't
|
|
3138
|
+
* require a second DB query.
|
|
3139
|
+
*
|
|
2933
3140
|
* @param cards - Cards to hydrate
|
|
2934
3141
|
* @returns Cards with tags populated
|
|
2935
3142
|
*/
|
|
@@ -2937,14 +3144,128 @@ var init_Pipeline = __esm({
|
|
|
2937
3144
|
if (cards.length === 0) {
|
|
2938
3145
|
return cards;
|
|
2939
3146
|
}
|
|
2940
|
-
const
|
|
2941
|
-
const
|
|
3147
|
+
const uncachedIds = [];
|
|
3148
|
+
for (const card of cards) {
|
|
3149
|
+
if (!this._tagCache.has(card.cardId)) {
|
|
3150
|
+
uncachedIds.push(card.cardId);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
if (uncachedIds.length > 0) {
|
|
3154
|
+
const freshTags = await this.course.getAppliedTagsBatch(uncachedIds);
|
|
3155
|
+
for (const [cardId, tags] of freshTags) {
|
|
3156
|
+
this._tagCache.set(cardId, tags);
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
3160
|
+
for (const card of cards) {
|
|
3161
|
+
tagsByCard.set(card.cardId, this._tagCache.get(card.cardId) ?? []);
|
|
3162
|
+
}
|
|
2942
3163
|
logTagHydration(cards, tagsByCard);
|
|
2943
3164
|
return cards.map((card) => ({
|
|
2944
3165
|
...card,
|
|
2945
|
-
tags:
|
|
3166
|
+
tags: this._tagCache.get(card.cardId) ?? []
|
|
2946
3167
|
}));
|
|
2947
3168
|
}
|
|
3169
|
+
// ---------------------------------------------------------------------------
|
|
3170
|
+
// Ephemeral hints application
|
|
3171
|
+
// ---------------------------------------------------------------------------
|
|
3172
|
+
/**
|
|
3173
|
+
* Apply one-shot replan hints to the post-filter card set.
|
|
3174
|
+
*
|
|
3175
|
+
* Order of operations:
|
|
3176
|
+
* 1. Exclude (remove unwanted cards)
|
|
3177
|
+
* 2. Boost (multiply scores)
|
|
3178
|
+
* 3. Require (inject must-have cards from the full pre-filter pool)
|
|
3179
|
+
*
|
|
3180
|
+
* @param cards - Post-filter cards (score > 0)
|
|
3181
|
+
* @param hints - The ephemeral hints to apply
|
|
3182
|
+
* @param allCards - Full pre-filter card pool (for require injection)
|
|
3183
|
+
*/
|
|
3184
|
+
applyHints(cards, hints, allCards) {
|
|
3185
|
+
const beforeCount = cards.length;
|
|
3186
|
+
if (hints.excludeCards?.length) {
|
|
3187
|
+
cards = cards.filter(
|
|
3188
|
+
(c) => !hints.excludeCards.some((pat) => globMatch(c.cardId, pat))
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
3191
|
+
if (hints.excludeTags?.length) {
|
|
3192
|
+
cards = cards.filter(
|
|
3193
|
+
(c) => !hints.excludeTags.some((pat) => cardMatchesTagPattern(c, pat))
|
|
3194
|
+
);
|
|
3195
|
+
}
|
|
3196
|
+
if (hints.boostTags) {
|
|
3197
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags)) {
|
|
3198
|
+
for (const card of cards) {
|
|
3199
|
+
if (cardMatchesTagPattern(card, pattern)) {
|
|
3200
|
+
card.score *= factor;
|
|
3201
|
+
card.provenance.push({
|
|
3202
|
+
strategy: "ephemeralHint",
|
|
3203
|
+
strategyId: "ephemeral-hint",
|
|
3204
|
+
strategyName: "Replan Hint",
|
|
3205
|
+
action: "boosted",
|
|
3206
|
+
score: card.score,
|
|
3207
|
+
reason: `boostTag ${pattern} \xD7${factor}`
|
|
3208
|
+
});
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
if (hints.boostCards) {
|
|
3214
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards)) {
|
|
3215
|
+
for (const card of cards) {
|
|
3216
|
+
if (globMatch(card.cardId, pattern)) {
|
|
3217
|
+
card.score *= factor;
|
|
3218
|
+
card.provenance.push({
|
|
3219
|
+
strategy: "ephemeralHint",
|
|
3220
|
+
strategyId: "ephemeral-hint",
|
|
3221
|
+
strategyName: "Replan Hint",
|
|
3222
|
+
action: "boosted",
|
|
3223
|
+
score: card.score,
|
|
3224
|
+
reason: `boostCard ${pattern} \xD7${factor}`
|
|
3225
|
+
});
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
}
|
|
3229
|
+
}
|
|
3230
|
+
const cardIds = new Set(cards.map((c) => c.cardId));
|
|
3231
|
+
const inject = (card, reason) => {
|
|
3232
|
+
if (!cardIds.has(card.cardId)) {
|
|
3233
|
+
const floorScore = Math.max(card.score, 1);
|
|
3234
|
+
cards.push({
|
|
3235
|
+
...card,
|
|
3236
|
+
score: floorScore,
|
|
3237
|
+
provenance: [
|
|
3238
|
+
...card.provenance,
|
|
3239
|
+
{
|
|
3240
|
+
strategy: "ephemeralHint",
|
|
3241
|
+
strategyId: "ephemeral-hint",
|
|
3242
|
+
strategyName: "Replan Hint",
|
|
3243
|
+
action: "boosted",
|
|
3244
|
+
score: floorScore,
|
|
3245
|
+
reason
|
|
3246
|
+
}
|
|
3247
|
+
]
|
|
3248
|
+
});
|
|
3249
|
+
cardIds.add(card.cardId);
|
|
3250
|
+
}
|
|
3251
|
+
};
|
|
3252
|
+
if (hints.requireCards?.length) {
|
|
3253
|
+
for (const pattern of hints.requireCards) {
|
|
3254
|
+
for (const card of allCards) {
|
|
3255
|
+
if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
|
|
3256
|
+
}
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
if (hints.requireTags?.length) {
|
|
3260
|
+
for (const pattern of hints.requireTags) {
|
|
3261
|
+
for (const card of allCards) {
|
|
3262
|
+
if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
3265
|
+
}
|
|
3266
|
+
logger.info(`[Pipeline] Hints applied: ${beforeCount} \u2192 ${cards.length} cards`);
|
|
3267
|
+
return cards;
|
|
3268
|
+
}
|
|
2948
3269
|
/**
|
|
2949
3270
|
* Build shared context for generator and filters.
|
|
2950
3271
|
*
|
|
@@ -2962,7 +3283,10 @@ var init_Pipeline = __esm({
|
|
|
2962
3283
|
} catch (e) {
|
|
2963
3284
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
2964
3285
|
}
|
|
2965
|
-
|
|
3286
|
+
if (!this._cachedOrchestration) {
|
|
3287
|
+
this._cachedOrchestration = await createOrchestrationContext(this.user, this.course);
|
|
3288
|
+
}
|
|
3289
|
+
const orchestration = this._cachedOrchestration;
|
|
2966
3290
|
return {
|
|
2967
3291
|
user: this.user,
|
|
2968
3292
|
course: this.course,
|
|
@@ -3006,6 +3330,87 @@ var init_Pipeline = __esm({
|
|
|
3006
3330
|
}
|
|
3007
3331
|
return [...new Set(ids)];
|
|
3008
3332
|
}
|
|
3333
|
+
// ---------------------------------------------------------------------------
|
|
3334
|
+
// Card-space diagnostic
|
|
3335
|
+
// ---------------------------------------------------------------------------
|
|
3336
|
+
/**
|
|
3337
|
+
* Scan every card in the course through the filter chain and report
|
|
3338
|
+
* how many are "well indicated" (score >= threshold) for the current user.
|
|
3339
|
+
*
|
|
3340
|
+
* Also reports how many well-indicated cards the user has NOT yet encountered.
|
|
3341
|
+
*
|
|
3342
|
+
* Exposed via `window.skuilder.pipeline.diagnoseCardSpace()`.
|
|
3343
|
+
*/
|
|
3344
|
+
async diagnoseCardSpace(opts) {
|
|
3345
|
+
const THRESHOLD = opts?.threshold ?? 0.1;
|
|
3346
|
+
const t0 = performance.now();
|
|
3347
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
3348
|
+
let cards = allCardIds.map((cardId) => ({
|
|
3349
|
+
cardId,
|
|
3350
|
+
courseId: this.course.getCourseID(),
|
|
3351
|
+
score: 1,
|
|
3352
|
+
provenance: []
|
|
3353
|
+
}));
|
|
3354
|
+
cards = await this.hydrateTags(cards);
|
|
3355
|
+
const context = await this.buildContext();
|
|
3356
|
+
const filterBreakdown = [];
|
|
3357
|
+
for (const filter of this.filters) {
|
|
3358
|
+
cards = await filter.transform(cards, context);
|
|
3359
|
+
const wi = cards.filter((c) => c.score >= THRESHOLD).length;
|
|
3360
|
+
filterBreakdown.push({ name: filter.name, wellIndicated: wi });
|
|
3361
|
+
}
|
|
3362
|
+
const wellIndicated = cards.filter((c) => c.score >= THRESHOLD);
|
|
3363
|
+
const wellIndicatedIds = new Set(wellIndicated.map((c) => c.cardId));
|
|
3364
|
+
let encounteredIds;
|
|
3365
|
+
try {
|
|
3366
|
+
const courseId = this.course.getCourseID();
|
|
3367
|
+
const seenCards = await this.user.getSeenCards(courseId);
|
|
3368
|
+
encounteredIds = new Set(seenCards);
|
|
3369
|
+
} catch {
|
|
3370
|
+
encounteredIds = /* @__PURE__ */ new Set();
|
|
3371
|
+
}
|
|
3372
|
+
const wellIndicatedNew = wellIndicated.filter((c) => !encounteredIds.has(c.cardId));
|
|
3373
|
+
const byType = /* @__PURE__ */ new Map();
|
|
3374
|
+
for (const card of cards) {
|
|
3375
|
+
const type = card.cardId.split("-")[1] || "unknown";
|
|
3376
|
+
if (!byType.has(type)) {
|
|
3377
|
+
byType.set(type, { total: 0, wellIndicated: 0, new: 0 });
|
|
3378
|
+
}
|
|
3379
|
+
const entry = byType.get(type);
|
|
3380
|
+
entry.total++;
|
|
3381
|
+
if (card.score >= THRESHOLD) {
|
|
3382
|
+
entry.wellIndicated++;
|
|
3383
|
+
if (!encounteredIds.has(card.cardId)) entry.new++;
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
const elapsed = performance.now() - t0;
|
|
3387
|
+
const result = {
|
|
3388
|
+
totalCards: allCardIds.length,
|
|
3389
|
+
threshold: THRESHOLD,
|
|
3390
|
+
wellIndicated: wellIndicatedIds.size,
|
|
3391
|
+
encountered: encounteredIds.size,
|
|
3392
|
+
wellIndicatedNew: wellIndicatedNew.length,
|
|
3393
|
+
byType: Object.fromEntries(byType),
|
|
3394
|
+
filterBreakdown,
|
|
3395
|
+
elapsedMs: Math.round(elapsed)
|
|
3396
|
+
};
|
|
3397
|
+
logger.info(`[Pipeline:diagnose] Card space scan (${result.elapsedMs}ms):`);
|
|
3398
|
+
logger.info(`[Pipeline:diagnose] Total cards: ${result.totalCards}`);
|
|
3399
|
+
logger.info(`[Pipeline:diagnose] Well-indicated (score >= ${THRESHOLD}): ${result.wellIndicated}`);
|
|
3400
|
+
logger.info(`[Pipeline:diagnose] Encountered: ${result.encountered}`);
|
|
3401
|
+
logger.info(`[Pipeline:diagnose] Well-indicated & new: ${result.wellIndicatedNew}`);
|
|
3402
|
+
logger.info(`[Pipeline:diagnose] By type:`);
|
|
3403
|
+
for (const [type, counts] of byType) {
|
|
3404
|
+
logger.info(
|
|
3405
|
+
`[Pipeline:diagnose] ${type}: ${counts.wellIndicated}/${counts.total} well-indicated, ${counts.new} new`
|
|
3406
|
+
);
|
|
3407
|
+
}
|
|
3408
|
+
logger.info(`[Pipeline:diagnose] After each filter:`);
|
|
3409
|
+
for (const fb of filterBreakdown) {
|
|
3410
|
+
logger.info(`[Pipeline:diagnose] ${fb.name}: ${fb.wellIndicated} well-indicated`);
|
|
3411
|
+
}
|
|
3412
|
+
return result;
|
|
3413
|
+
}
|
|
3009
3414
|
};
|
|
3010
3415
|
}
|
|
3011
3416
|
});
|
|
@@ -3110,23 +3515,25 @@ var init_PipelineAssembler = __esm({
|
|
|
3110
3515
|
warnings.push(`Unknown strategy type '${s.implementingClass}', skipping: ${s.name}`);
|
|
3111
3516
|
}
|
|
3112
3517
|
}
|
|
3518
|
+
const courseId = course.getCourseID();
|
|
3519
|
+
const hasElo = generatorStrategies.some((s) => s.implementingClass === "elo" /* ELO */);
|
|
3520
|
+
const hasSrs = generatorStrategies.some((s) => s.implementingClass === "srs" /* SRS */);
|
|
3521
|
+
if (!hasElo) {
|
|
3522
|
+
logger.debug("[PipelineAssembler] No ELO generator configured, adding default");
|
|
3523
|
+
generatorStrategies.push(createDefaultEloStrategy(courseId));
|
|
3524
|
+
}
|
|
3525
|
+
if (!hasSrs) {
|
|
3526
|
+
logger.debug("[PipelineAssembler] No SRS generator configured, adding default");
|
|
3527
|
+
generatorStrategies.push(createDefaultSrsStrategy(courseId));
|
|
3528
|
+
}
|
|
3113
3529
|
if (generatorStrategies.length === 0) {
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
} else {
|
|
3122
|
-
warnings.push("No generator strategy found");
|
|
3123
|
-
return {
|
|
3124
|
-
pipeline: null,
|
|
3125
|
-
generatorStrategies: [],
|
|
3126
|
-
filterStrategies: [],
|
|
3127
|
-
warnings
|
|
3128
|
-
};
|
|
3129
|
-
}
|
|
3530
|
+
warnings.push("No generator strategy found");
|
|
3531
|
+
return {
|
|
3532
|
+
pipeline: null,
|
|
3533
|
+
generatorStrategies: [],
|
|
3534
|
+
filterStrategies: [],
|
|
3535
|
+
warnings
|
|
3536
|
+
};
|
|
3130
3537
|
}
|
|
3131
3538
|
let generator;
|
|
3132
3539
|
if (generatorStrategies.length === 1) {
|
|
@@ -3204,6 +3611,7 @@ var init_3 = __esm({
|
|
|
3204
3611
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
3205
3612
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
3206
3613
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
3614
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
3207
3615
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
3208
3616
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
3209
3617
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
@@ -3252,8 +3660,10 @@ async function initializeNavigatorRegistry() {
|
|
|
3252
3660
|
Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
3253
3661
|
Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
3254
3662
|
]);
|
|
3663
|
+
const prescribedModule = await Promise.resolve().then(() => (init_prescribed(), prescribed_exports));
|
|
3255
3664
|
registerNavigator("elo", eloModule.default);
|
|
3256
3665
|
registerNavigator("srs", srsModule.default);
|
|
3666
|
+
registerNavigator("prescribed", prescribedModule.default);
|
|
3257
3667
|
const [
|
|
3258
3668
|
hierarchyModule,
|
|
3259
3669
|
interferenceModule,
|
|
@@ -3308,6 +3718,7 @@ var init_navigators = __esm({
|
|
|
3308
3718
|
Navigators = /* @__PURE__ */ ((Navigators2) => {
|
|
3309
3719
|
Navigators2["ELO"] = "elo";
|
|
3310
3720
|
Navigators2["SRS"] = "srs";
|
|
3721
|
+
Navigators2["PRESCRIBED"] = "prescribed";
|
|
3311
3722
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
3312
3723
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
3313
3724
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
@@ -3322,6 +3733,7 @@ var init_navigators = __esm({
|
|
|
3322
3733
|
NavigatorRoles = {
|
|
3323
3734
|
["elo" /* ELO */]: "generator" /* GENERATOR */,
|
|
3324
3735
|
["srs" /* SRS */]: "generator" /* GENERATOR */,
|
|
3736
|
+
["prescribed" /* PRESCRIBED */]: "generator" /* GENERATOR */,
|
|
3325
3737
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
3326
3738
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
3327
3739
|
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
@@ -3486,6 +3898,12 @@ var init_navigators = __esm({
|
|
|
3486
3898
|
async getWeightedCards(_limit) {
|
|
3487
3899
|
throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
|
|
3488
3900
|
}
|
|
3901
|
+
/**
|
|
3902
|
+
* Set ephemeral hints for the next pipeline run.
|
|
3903
|
+
* No-op for non-Pipeline navigators. Pipeline overrides this.
|
|
3904
|
+
*/
|
|
3905
|
+
setEphemeralHints(_hints) {
|
|
3906
|
+
}
|
|
3489
3907
|
};
|
|
3490
3908
|
}
|
|
3491
3909
|
});
|
|
@@ -3590,15 +4008,42 @@ var init_courseDB = __esm({
|
|
|
3590
4008
|
// private log(msg: string): void {
|
|
3591
4009
|
// log(`CourseLog: ${this.id}\n ${msg}`);
|
|
3592
4010
|
// }
|
|
4011
|
+
/**
|
|
4012
|
+
* Primary database handle used for all **read** operations (queries, gets).
|
|
4013
|
+
*
|
|
4014
|
+
* When local sync is active, this points to the local PouchDB replica for
|
|
4015
|
+
* fast, network-free reads. Otherwise it points to the remote CouchDB.
|
|
4016
|
+
*/
|
|
3593
4017
|
db;
|
|
4018
|
+
/**
|
|
4019
|
+
* Remote database handle used for all **write** operations.
|
|
4020
|
+
*
|
|
4021
|
+
* Always points to the remote CouchDB so that writes (ELO updates, tag
|
|
4022
|
+
* mutations, admin operations) aggregate on the server. The local replica
|
|
4023
|
+
* is a read-only snapshot that refreshes on the next page load.
|
|
4024
|
+
*
|
|
4025
|
+
* When local sync is NOT active, this is the same instance as `this.db`.
|
|
4026
|
+
*/
|
|
4027
|
+
remoteDB;
|
|
3594
4028
|
id;
|
|
3595
4029
|
_getCurrentUser;
|
|
3596
4030
|
updateQueue;
|
|
3597
|
-
|
|
4031
|
+
/**
|
|
4032
|
+
* @param id - Course ID
|
|
4033
|
+
* @param userLookup - Async function returning the current user DB
|
|
4034
|
+
* @param localDB - Optional local PouchDB replica for reads. When provided,
|
|
4035
|
+
* `this.db` uses the local replica and `this.remoteDB` stays remote.
|
|
4036
|
+
* The UpdateQueue reads from remote and writes to remote (local `_rev`
|
|
4037
|
+
* values may be stale, so read-modify-write cycles must go through
|
|
4038
|
+
* the remote DB to avoid conflicts).
|
|
4039
|
+
*/
|
|
4040
|
+
constructor(id, userLookup, localDB) {
|
|
3598
4041
|
this.id = id;
|
|
3599
|
-
|
|
4042
|
+
const remote = getCourseDB2(this.id);
|
|
4043
|
+
this.remoteDB = remote;
|
|
4044
|
+
this.db = localDB ?? remote;
|
|
3600
4045
|
this._getCurrentUser = userLookup;
|
|
3601
|
-
this.updateQueue = new UpdateQueue(this.
|
|
4046
|
+
this.updateQueue = new UpdateQueue(this.remoteDB, this.remoteDB);
|
|
3602
4047
|
}
|
|
3603
4048
|
getCourseID() {
|
|
3604
4049
|
return this.id;
|
|
@@ -3686,7 +4131,7 @@ var init_courseDB = __esm({
|
|
|
3686
4131
|
};
|
|
3687
4132
|
}
|
|
3688
4133
|
async removeCard(id) {
|
|
3689
|
-
const doc = await this.
|
|
4134
|
+
const doc = await this.remoteDB.get(id);
|
|
3690
4135
|
if (!doc.docType || !(doc.docType === "CARD" /* CARD */)) {
|
|
3691
4136
|
throw new Error(`failed to remove ${id} from course ${this.id}. id does not point to a card`);
|
|
3692
4137
|
}
|
|
@@ -3707,7 +4152,7 @@ var init_courseDB = __esm({
|
|
|
3707
4152
|
} catch (error) {
|
|
3708
4153
|
logger.error(`Error removing card ${id} from tags: ${error}`);
|
|
3709
4154
|
}
|
|
3710
|
-
return this.
|
|
4155
|
+
return this.remoteDB.remove(doc);
|
|
3711
4156
|
}
|
|
3712
4157
|
async getCardDisplayableDataIDs(id) {
|
|
3713
4158
|
logger.debug(id.join(", "));
|
|
@@ -3809,8 +4254,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3809
4254
|
if (cardIds.length === 0) {
|
|
3810
4255
|
return /* @__PURE__ */ new Map();
|
|
3811
4256
|
}
|
|
3812
|
-
const
|
|
3813
|
-
const result = await db.query("getTags", {
|
|
4257
|
+
const result = await this.db.query("getTags", {
|
|
3814
4258
|
keys: cardIds,
|
|
3815
4259
|
include_docs: false
|
|
3816
4260
|
});
|
|
@@ -3827,6 +4271,14 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3827
4271
|
}
|
|
3828
4272
|
return tagsByCard;
|
|
3829
4273
|
}
|
|
4274
|
+
async getAllCardIds() {
|
|
4275
|
+
const result = await this.db.allDocs({
|
|
4276
|
+
startkey: "CARD-",
|
|
4277
|
+
endkey: "CARD-\uFFF0",
|
|
4278
|
+
include_docs: false
|
|
4279
|
+
});
|
|
4280
|
+
return result.rows.map((row) => row.id);
|
|
4281
|
+
}
|
|
3830
4282
|
async addTagToCard(cardId, tagId, updateELO) {
|
|
3831
4283
|
return await addTagToCard(
|
|
3832
4284
|
this.id,
|
|
@@ -3893,10 +4345,13 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3893
4345
|
}
|
|
3894
4346
|
}
|
|
3895
4347
|
async getCourseDoc(id, options) {
|
|
3896
|
-
return await
|
|
4348
|
+
return await this.db.get(id, options);
|
|
3897
4349
|
}
|
|
3898
4350
|
async getCourseDocs(ids, options = {}) {
|
|
3899
|
-
return await
|
|
4351
|
+
return await this.db.allDocs({
|
|
4352
|
+
...options,
|
|
4353
|
+
keys: ids
|
|
4354
|
+
});
|
|
3900
4355
|
}
|
|
3901
4356
|
////////////////////////////////////
|
|
3902
4357
|
// NavigationStrategyManager implementation
|
|
@@ -3930,7 +4385,7 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
|
|
|
3930
4385
|
}
|
|
3931
4386
|
async addNavigationStrategy(data) {
|
|
3932
4387
|
logger.debug(`[courseDB] Adding navigation strategy: ${data._id}`);
|
|
3933
|
-
return this.
|
|
4388
|
+
return this.remoteDB.put(data).then(() => {
|
|
3934
4389
|
});
|
|
3935
4390
|
}
|
|
3936
4391
|
updateNavigationStrategy(id, data) {
|
|
@@ -4346,6 +4801,16 @@ var init_adminDB2 = __esm({
|
|
|
4346
4801
|
}
|
|
4347
4802
|
});
|
|
4348
4803
|
|
|
4804
|
+
// src/impl/couch/CourseSyncService.ts
|
|
4805
|
+
var init_CourseSyncService = __esm({
|
|
4806
|
+
"src/impl/couch/CourseSyncService.ts"() {
|
|
4807
|
+
"use strict";
|
|
4808
|
+
init_pouchdb_setup();
|
|
4809
|
+
init_couch();
|
|
4810
|
+
init_logger();
|
|
4811
|
+
}
|
|
4812
|
+
});
|
|
4813
|
+
|
|
4349
4814
|
// src/impl/couch/auth.ts
|
|
4350
4815
|
import fetch from "cross-fetch";
|
|
4351
4816
|
var init_auth = __esm({
|
|
@@ -4400,15 +4865,6 @@ function getCourseDB2(courseID) {
|
|
|
4400
4865
|
createPouchDBConfig()
|
|
4401
4866
|
);
|
|
4402
4867
|
}
|
|
4403
|
-
function getCourseDocs(courseID, docIDs, options = {}) {
|
|
4404
|
-
return getCourseDB2(courseID).allDocs({
|
|
4405
|
-
...options,
|
|
4406
|
-
keys: docIDs
|
|
4407
|
-
});
|
|
4408
|
-
}
|
|
4409
|
-
function getCourseDoc(courseID, docID, options = {}) {
|
|
4410
|
-
return getCourseDB2(courseID).get(docID, options);
|
|
4411
|
-
}
|
|
4412
4868
|
function filterAllDocsByPrefix2(db, prefix, opts) {
|
|
4413
4869
|
const options = {
|
|
4414
4870
|
startkey: prefix,
|
|
@@ -4439,6 +4895,7 @@ var init_couch = __esm({
|
|
|
4439
4895
|
init_classroomDB2();
|
|
4440
4896
|
init_courseAPI();
|
|
4441
4897
|
init_courseDB();
|
|
4898
|
+
init_CourseSyncService();
|
|
4442
4899
|
init_CouchDBSyncStrategy();
|
|
4443
4900
|
isBrowser = typeof window !== "undefined";
|
|
4444
4901
|
if (isBrowser) {
|
|
@@ -4658,6 +5115,9 @@ Currently logged-in as ${this._username}.`
|
|
|
4658
5115
|
const id = row.id;
|
|
4659
5116
|
return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
|
|
4660
5117
|
id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
|
|
5118
|
+
id.startsWith(DocTypePrefixes["STRATEGY_STATE" /* STRATEGY_STATE */]) || // Strategy state (user prefs, progression)
|
|
5119
|
+
id.startsWith(DocTypePrefixes["USER_OUTCOME" /* USER_OUTCOME */]) || // Evolutionary orchestration outcomes
|
|
5120
|
+
id.startsWith(DocTypePrefixes["STRATEGY_LEARNING_STATE" /* STRATEGY_LEARNING_STATE */]) || // Strategy learning state
|
|
4661
5121
|
id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
4662
5122
|
id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
4663
5123
|
id === _BaseUser.DOC_IDS.CONFIG;
|