@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
|
@@ -504,8 +504,12 @@ __export(PipelineDebugger_exports, {
|
|
|
504
504
|
buildRunReport: () => buildRunReport,
|
|
505
505
|
captureRun: () => captureRun,
|
|
506
506
|
mountPipelineDebugger: () => mountPipelineDebugger,
|
|
507
|
-
pipelineDebugAPI: () => pipelineDebugAPI
|
|
507
|
+
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
508
|
+
registerPipelineForDebug: () => registerPipelineForDebug
|
|
508
509
|
});
|
|
510
|
+
function registerPipelineForDebug(pipeline) {
|
|
511
|
+
_activePipeline = pipeline;
|
|
512
|
+
}
|
|
509
513
|
function getOrigin(card) {
|
|
510
514
|
const firstEntry = card.provenance[0];
|
|
511
515
|
if (!firstEntry) return "unknown";
|
|
@@ -533,6 +537,7 @@ function buildRunReport(courseId, courseName, generatorName, generators, generat
|
|
|
533
537
|
origin: getOrigin(card),
|
|
534
538
|
finalScore: card.score,
|
|
535
539
|
provenance: card.provenance,
|
|
540
|
+
tags: card.tags,
|
|
536
541
|
selected: selectedIds.has(card.cardId)
|
|
537
542
|
}));
|
|
538
543
|
const reviewsSelected = selectedCards.filter((c) => getOrigin(c) === "review").length;
|
|
@@ -588,12 +593,13 @@ function mountPipelineDebugger() {
|
|
|
588
593
|
win.skuilder = win.skuilder || {};
|
|
589
594
|
win.skuilder.pipeline = pipelineDebugAPI;
|
|
590
595
|
}
|
|
591
|
-
var MAX_RUNS, runHistory, pipelineDebugAPI;
|
|
596
|
+
var _activePipeline, MAX_RUNS, runHistory, pipelineDebugAPI;
|
|
592
597
|
var init_PipelineDebugger = __esm({
|
|
593
598
|
"src/core/navigators/PipelineDebugger.ts"() {
|
|
594
599
|
"use strict";
|
|
595
600
|
init_navigators();
|
|
596
601
|
init_logger();
|
|
602
|
+
_activePipeline = null;
|
|
597
603
|
MAX_RUNS = 10;
|
|
598
604
|
runHistory = [];
|
|
599
605
|
pipelineDebugAPI = {
|
|
@@ -795,6 +801,21 @@ var init_PipelineDebugger = __esm({
|
|
|
795
801
|
}
|
|
796
802
|
console.groupEnd();
|
|
797
803
|
},
|
|
804
|
+
/**
|
|
805
|
+
* Scan the full card space through the filter chain for the current user.
|
|
806
|
+
*
|
|
807
|
+
* Reports how many cards are well-indicated and how many are new.
|
|
808
|
+
* Use this to understand how the search space grows during onboarding.
|
|
809
|
+
*
|
|
810
|
+
* @param threshold - Score threshold for "well indicated" (default 0.10)
|
|
811
|
+
*/
|
|
812
|
+
async diagnoseCardSpace(threshold) {
|
|
813
|
+
if (!_activePipeline) {
|
|
814
|
+
logger.info("[Pipeline Debug] No active pipeline. Run a session first.");
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
return _activePipeline.diagnoseCardSpace({ threshold });
|
|
818
|
+
},
|
|
798
819
|
/**
|
|
799
820
|
* Show help.
|
|
800
821
|
*/
|
|
@@ -807,6 +828,7 @@ Commands:
|
|
|
807
828
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
808
829
|
.showCard(cardId) Show provenance trail for a specific card
|
|
809
830
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
831
|
+
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
810
832
|
.showRegistry() Show navigator registry (classes + roles)
|
|
811
833
|
.showStrategies() Show registry + strategy mapping from last run
|
|
812
834
|
.listRuns() List all captured runs in table format
|
|
@@ -818,7 +840,7 @@ Commands:
|
|
|
818
840
|
Example:
|
|
819
841
|
window.skuilder.pipeline.showLastRun()
|
|
820
842
|
window.skuilder.pipeline.showRun(1)
|
|
821
|
-
window.skuilder.pipeline.
|
|
843
|
+
await window.skuilder.pipeline.diagnoseCardSpace()
|
|
822
844
|
`);
|
|
823
845
|
}
|
|
824
846
|
};
|
|
@@ -1113,6 +1135,69 @@ var init_generators = __esm({
|
|
|
1113
1135
|
}
|
|
1114
1136
|
});
|
|
1115
1137
|
|
|
1138
|
+
// src/core/navigators/generators/prescribed.ts
|
|
1139
|
+
var prescribed_exports = {};
|
|
1140
|
+
__export(prescribed_exports, {
|
|
1141
|
+
default: () => PrescribedCardsGenerator
|
|
1142
|
+
});
|
|
1143
|
+
var PrescribedCardsGenerator;
|
|
1144
|
+
var init_prescribed = __esm({
|
|
1145
|
+
"src/core/navigators/generators/prescribed.ts"() {
|
|
1146
|
+
"use strict";
|
|
1147
|
+
init_navigators();
|
|
1148
|
+
init_logger();
|
|
1149
|
+
PrescribedCardsGenerator = class extends ContentNavigator {
|
|
1150
|
+
name;
|
|
1151
|
+
config;
|
|
1152
|
+
constructor(user, course, strategyData) {
|
|
1153
|
+
super(user, course, strategyData);
|
|
1154
|
+
this.name = strategyData.name || "Prescribed Cards";
|
|
1155
|
+
try {
|
|
1156
|
+
const parsed = JSON.parse(strategyData.serializedData);
|
|
1157
|
+
this.config = { cardIds: parsed.cardIds || [] };
|
|
1158
|
+
} catch {
|
|
1159
|
+
this.config = { cardIds: [] };
|
|
1160
|
+
}
|
|
1161
|
+
logger.debug(
|
|
1162
|
+
`[Prescribed] Initialized with ${this.config.cardIds.length} prescribed cards`
|
|
1163
|
+
);
|
|
1164
|
+
}
|
|
1165
|
+
async getWeightedCards(limit, _context) {
|
|
1166
|
+
if (this.config.cardIds.length === 0) {
|
|
1167
|
+
return [];
|
|
1168
|
+
}
|
|
1169
|
+
const courseId = this.course.getCourseID();
|
|
1170
|
+
const activeCards = await this.user.getActiveCards();
|
|
1171
|
+
const activeIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
1172
|
+
const eligibleIds = this.config.cardIds.filter((id) => !activeIds.has(id));
|
|
1173
|
+
if (eligibleIds.length === 0) {
|
|
1174
|
+
logger.debug("[Prescribed] All prescribed cards already active, returning empty");
|
|
1175
|
+
return [];
|
|
1176
|
+
}
|
|
1177
|
+
const cards = eligibleIds.slice(0, limit).map((cardId) => ({
|
|
1178
|
+
cardId,
|
|
1179
|
+
courseId,
|
|
1180
|
+
score: 1,
|
|
1181
|
+
provenance: [
|
|
1182
|
+
{
|
|
1183
|
+
strategy: "prescribed",
|
|
1184
|
+
strategyName: this.strategyName || this.name,
|
|
1185
|
+
strategyId: this.strategyId || "NAVIGATION_STRATEGY-prescribed",
|
|
1186
|
+
action: "generated",
|
|
1187
|
+
score: 1,
|
|
1188
|
+
reason: `Prescribed card (${eligibleIds.length} eligible of ${this.config.cardIds.length} configured)`
|
|
1189
|
+
}
|
|
1190
|
+
]
|
|
1191
|
+
}));
|
|
1192
|
+
logger.info(
|
|
1193
|
+
`[Prescribed] Emitting ${cards.length} cards (${eligibleIds.length} eligible, ${activeIds.size} already active)`
|
|
1194
|
+
);
|
|
1195
|
+
return cards;
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
|
|
1116
1201
|
// src/core/navigators/generators/srs.ts
|
|
1117
1202
|
var srs_exports = {};
|
|
1118
1203
|
__export(srs_exports, {
|
|
@@ -1307,6 +1392,7 @@ var init_ = __esm({
|
|
|
1307
1392
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
1308
1393
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
1309
1394
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
1395
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
1310
1396
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
1311
1397
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports))
|
|
1312
1398
|
});
|
|
@@ -1507,6 +1593,8 @@ var init_hierarchyDefinition = __esm({
|
|
|
1507
1593
|
if (userTagElo.count < minCount) return false;
|
|
1508
1594
|
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1509
1595
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
1596
|
+
} else if (prereq.masteryThreshold?.minCount !== void 0) {
|
|
1597
|
+
return true;
|
|
1510
1598
|
} else {
|
|
1511
1599
|
return userTagElo.score >= userGlobalElo;
|
|
1512
1600
|
}
|
|
@@ -1582,14 +1670,38 @@ var init_hierarchyDefinition = __esm({
|
|
|
1582
1670
|
};
|
|
1583
1671
|
}
|
|
1584
1672
|
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Build a map of prereq tag → max configured boost for all *closed* gates.
|
|
1675
|
+
*
|
|
1676
|
+
* When a gate is closed (prereqs unmet), cards carrying that gate's prereq
|
|
1677
|
+
* tags get boosted — steering the pipeline toward content that helps unlock
|
|
1678
|
+
* the gated material. Once the gate opens, the boost disappears.
|
|
1679
|
+
*/
|
|
1680
|
+
getPreReqBoosts(unlockedTags, masteredTags) {
|
|
1681
|
+
const boosts = /* @__PURE__ */ new Map();
|
|
1682
|
+
for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
|
|
1683
|
+
if (unlockedTags.has(tagId)) continue;
|
|
1684
|
+
for (const prereq of prereqs) {
|
|
1685
|
+
if (!prereq.preReqBoost || prereq.preReqBoost <= 1) continue;
|
|
1686
|
+
if (masteredTags.has(prereq.tag)) continue;
|
|
1687
|
+
const existing = boosts.get(prereq.tag) ?? 1;
|
|
1688
|
+
boosts.set(prereq.tag, Math.max(existing, prereq.preReqBoost));
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
return boosts;
|
|
1692
|
+
}
|
|
1585
1693
|
/**
|
|
1586
1694
|
* CardFilter.transform implementation.
|
|
1587
1695
|
*
|
|
1588
|
-
*
|
|
1696
|
+
* Two effects:
|
|
1697
|
+
* 1. Cards with locked tags receive score * 0.05 (gating penalty)
|
|
1698
|
+
* 2. Cards carrying prereq tags of closed gates receive a configured
|
|
1699
|
+
* boost (preReqBoost), steering toward content that unlocks gates
|
|
1589
1700
|
*/
|
|
1590
1701
|
async transform(cards, context) {
|
|
1591
1702
|
const masteredTags = await this.getMasteredTags(context);
|
|
1592
1703
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
1704
|
+
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
1593
1705
|
const gated = [];
|
|
1594
1706
|
for (const card of cards) {
|
|
1595
1707
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
@@ -1598,9 +1710,27 @@ var init_hierarchyDefinition = __esm({
|
|
|
1598
1710
|
unlockedTags,
|
|
1599
1711
|
masteredTags
|
|
1600
1712
|
);
|
|
1601
|
-
const LOCKED_PENALTY = 0.
|
|
1602
|
-
|
|
1603
|
-
|
|
1713
|
+
const LOCKED_PENALTY = 0.02;
|
|
1714
|
+
let finalScore = isUnlocked ? card.score : card.score * LOCKED_PENALTY;
|
|
1715
|
+
let action = isUnlocked ? "passed" : "penalized";
|
|
1716
|
+
let finalReason = reason;
|
|
1717
|
+
if (isUnlocked && preReqBoosts.size > 0) {
|
|
1718
|
+
const cardTags = card.tags ?? [];
|
|
1719
|
+
let maxBoost = 1;
|
|
1720
|
+
const boostedPrereqs = [];
|
|
1721
|
+
for (const tag of cardTags) {
|
|
1722
|
+
const boost = preReqBoosts.get(tag);
|
|
1723
|
+
if (boost && boost > maxBoost) {
|
|
1724
|
+
maxBoost = boost;
|
|
1725
|
+
boostedPrereqs.push(tag);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
if (maxBoost > 1) {
|
|
1729
|
+
finalScore *= maxBoost;
|
|
1730
|
+
action = "boosted";
|
|
1731
|
+
finalReason = `${reason} | preReqBoost \xD7${maxBoost.toFixed(2)} for ${boostedPrereqs.join(", ")}`;
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1604
1734
|
gated.push({
|
|
1605
1735
|
...card,
|
|
1606
1736
|
score: finalScore,
|
|
@@ -1612,7 +1742,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1612
1742
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-hierarchy",
|
|
1613
1743
|
action,
|
|
1614
1744
|
score: finalScore,
|
|
1615
|
-
reason
|
|
1745
|
+
reason: finalReason
|
|
1616
1746
|
}
|
|
1617
1747
|
]
|
|
1618
1748
|
});
|
|
@@ -2301,6 +2431,18 @@ __export(Pipeline_exports, {
|
|
|
2301
2431
|
Pipeline: () => Pipeline
|
|
2302
2432
|
});
|
|
2303
2433
|
import { toCourseElo as toCourseElo5 } from "@vue-skuilder/common";
|
|
2434
|
+
function globToRegex(pattern) {
|
|
2435
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&");
|
|
2436
|
+
const withWildcards = escaped.replace(/\*/g, ".*");
|
|
2437
|
+
return new RegExp(`^${withWildcards}$`);
|
|
2438
|
+
}
|
|
2439
|
+
function globMatch(value, pattern) {
|
|
2440
|
+
if (!pattern.includes("*")) return value === pattern;
|
|
2441
|
+
return globToRegex(pattern).test(value);
|
|
2442
|
+
}
|
|
2443
|
+
function cardMatchesTagPattern(card, pattern) {
|
|
2444
|
+
return (card.tags ?? []).some((tag) => globMatch(tag, pattern));
|
|
2445
|
+
}
|
|
2304
2446
|
function logPipelineConfig(generator, filters) {
|
|
2305
2447
|
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
2306
2448
|
logger.info(
|
|
@@ -2335,6 +2477,21 @@ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCo
|
|
|
2335
2477
|
\u{1F4A1} Inspect: window.skuilder.pipeline`
|
|
2336
2478
|
);
|
|
2337
2479
|
}
|
|
2480
|
+
function logResultCards(cards) {
|
|
2481
|
+
if (!VERBOSE_RESULTS || cards.length === 0) return;
|
|
2482
|
+
logger.info(`[Pipeline] Results (${cards.length} cards):`);
|
|
2483
|
+
for (let i = 0; i < cards.length; i++) {
|
|
2484
|
+
const c = cards[i];
|
|
2485
|
+
const tags = c.tags?.slice(0, 3).join(", ") || "";
|
|
2486
|
+
const filters = c.provenance.filter((p) => p.strategy === "hierarchyDefinition" || p.strategy === "priorityDefinition" || p.strategy === "interferenceFilter" || p.strategy === "letterGating" || p.strategy === "ephemeralHint").map((p) => {
|
|
2487
|
+
const arrow = p.action === "boosted" ? "\u2191" : p.action === "penalized" ? "\u2193" : "=";
|
|
2488
|
+
return `${p.strategyName}${arrow}${p.score.toFixed(2)}`;
|
|
2489
|
+
}).join(" | ");
|
|
2490
|
+
logger.info(
|
|
2491
|
+
`[Pipeline] ${String(i + 1).padStart(2)}. ${c.score.toFixed(4)} ${c.cardId} [${tags}]${filters ? ` {${filters}}` : ""}`
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
}
|
|
2338
2495
|
function logCardProvenance(cards, maxCards = 3) {
|
|
2339
2496
|
const cardsToLog = cards.slice(0, maxCards);
|
|
2340
2497
|
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
@@ -2349,7 +2506,7 @@ function logCardProvenance(cards, maxCards = 3) {
|
|
|
2349
2506
|
}
|
|
2350
2507
|
}
|
|
2351
2508
|
}
|
|
2352
|
-
var Pipeline;
|
|
2509
|
+
var VERBOSE_RESULTS, Pipeline;
|
|
2353
2510
|
var init_Pipeline = __esm({
|
|
2354
2511
|
"src/core/navigators/Pipeline.ts"() {
|
|
2355
2512
|
"use strict";
|
|
@@ -2357,9 +2514,31 @@ var init_Pipeline = __esm({
|
|
|
2357
2514
|
init_logger();
|
|
2358
2515
|
init_orchestration();
|
|
2359
2516
|
init_PipelineDebugger();
|
|
2517
|
+
VERBOSE_RESULTS = true;
|
|
2360
2518
|
Pipeline = class extends ContentNavigator {
|
|
2361
2519
|
generator;
|
|
2362
2520
|
filters;
|
|
2521
|
+
/**
|
|
2522
|
+
* Cached orchestration context. Course config and salt don't change within
|
|
2523
|
+
* a page load, so we build the orchestration context once and reuse it on
|
|
2524
|
+
* subsequent getWeightedCards() calls (e.g. mid-session replans).
|
|
2525
|
+
*
|
|
2526
|
+
* This eliminates a remote getCourseConfig() round trip per pipeline run.
|
|
2527
|
+
*/
|
|
2528
|
+
_cachedOrchestration = null;
|
|
2529
|
+
/**
|
|
2530
|
+
* Persistent tag cache. Maps cardId → tag names.
|
|
2531
|
+
*
|
|
2532
|
+
* Tags are static within a session (they're set at card generation time),
|
|
2533
|
+
* so we cache them across pipeline runs. On replans, many of the same cards
|
|
2534
|
+
* reappear — cache hits avoid redundant remote getAppliedTagsBatch() queries.
|
|
2535
|
+
*/
|
|
2536
|
+
_tagCache = /* @__PURE__ */ new Map();
|
|
2537
|
+
/**
|
|
2538
|
+
* One-shot replan hints. Applied after the filter chain on the next
|
|
2539
|
+
* getWeightedCards() call, then cleared.
|
|
2540
|
+
*/
|
|
2541
|
+
_ephemeralHints = null;
|
|
2363
2542
|
/**
|
|
2364
2543
|
* Create a new pipeline.
|
|
2365
2544
|
*
|
|
@@ -2380,6 +2559,17 @@ var init_Pipeline = __esm({
|
|
|
2380
2559
|
logger.error(`[pipeline] Failed to lookup courseCfg: ${e}`);
|
|
2381
2560
|
});
|
|
2382
2561
|
logPipelineConfig(generator, filters);
|
|
2562
|
+
registerPipelineForDebug(this);
|
|
2563
|
+
}
|
|
2564
|
+
/**
|
|
2565
|
+
* Set one-shot hints for the next pipeline run.
|
|
2566
|
+
* Consumed after one getWeightedCards() call, then cleared.
|
|
2567
|
+
*
|
|
2568
|
+
* Overrides ContentNavigator.setEphemeralHints() no-op.
|
|
2569
|
+
*/
|
|
2570
|
+
setEphemeralHints(hints) {
|
|
2571
|
+
this._ephemeralHints = hints;
|
|
2572
|
+
logger.info(`[Pipeline] Ephemeral hints set: ${JSON.stringify(hints)}`);
|
|
2383
2573
|
}
|
|
2384
2574
|
/**
|
|
2385
2575
|
* Get weighted cards by running generator and applying filters.
|
|
@@ -2396,13 +2586,15 @@ var init_Pipeline = __esm({
|
|
|
2396
2586
|
* @returns Cards sorted by score descending
|
|
2397
2587
|
*/
|
|
2398
2588
|
async getWeightedCards(limit) {
|
|
2589
|
+
const t0 = performance.now();
|
|
2399
2590
|
const context = await this.buildContext();
|
|
2400
|
-
const
|
|
2401
|
-
const fetchLimit =
|
|
2591
|
+
const tContext = performance.now();
|
|
2592
|
+
const fetchLimit = 500;
|
|
2402
2593
|
logger.debug(
|
|
2403
2594
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
2404
2595
|
);
|
|
2405
2596
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
2597
|
+
const tGenerate = performance.now();
|
|
2406
2598
|
const generatedCount = cards.length;
|
|
2407
2599
|
let generatorSummaries;
|
|
2408
2600
|
if (this.generator.generators) {
|
|
@@ -2431,6 +2623,7 @@ var init_Pipeline = __esm({
|
|
|
2431
2623
|
}
|
|
2432
2624
|
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
2433
2625
|
cards = await this.hydrateTags(cards);
|
|
2626
|
+
const tHydrate = performance.now();
|
|
2434
2627
|
const allCardsBeforeFiltering = [...cards];
|
|
2435
2628
|
const filterImpacts = [];
|
|
2436
2629
|
for (const filter of this.filters) {
|
|
@@ -2449,8 +2642,17 @@ var init_Pipeline = __esm({
|
|
|
2449
2642
|
logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
|
|
2450
2643
|
}
|
|
2451
2644
|
cards = cards.filter((c) => c.score > 0);
|
|
2645
|
+
const hints = this._ephemeralHints;
|
|
2646
|
+
if (hints) {
|
|
2647
|
+
this._ephemeralHints = null;
|
|
2648
|
+
cards = this.applyHints(cards, hints, allCardsBeforeFiltering);
|
|
2649
|
+
}
|
|
2452
2650
|
cards.sort((a, b) => b.score - a.score);
|
|
2651
|
+
const tFilter = performance.now();
|
|
2453
2652
|
const result = cards.slice(0, limit);
|
|
2653
|
+
logger.info(
|
|
2654
|
+
`[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)})`
|
|
2655
|
+
);
|
|
2454
2656
|
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
2455
2657
|
logExecutionSummary(
|
|
2456
2658
|
this.generator.name,
|
|
@@ -2460,6 +2662,7 @@ var init_Pipeline = __esm({
|
|
|
2460
2662
|
topScores,
|
|
2461
2663
|
filterImpacts
|
|
2462
2664
|
);
|
|
2665
|
+
logResultCards(result);
|
|
2463
2666
|
logCardProvenance(result, 3);
|
|
2464
2667
|
try {
|
|
2465
2668
|
const courseName = await this.course?.getCourseConfig().then((c) => c.name).catch(() => void 0);
|
|
@@ -2486,6 +2689,10 @@ var init_Pipeline = __esm({
|
|
|
2486
2689
|
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
2487
2690
|
* making individual getAppliedTags() calls.
|
|
2488
2691
|
*
|
|
2692
|
+
* Uses a persistent tag cache across pipeline runs — tags are static within
|
|
2693
|
+
* a session, so cards seen in a prior run (e.g. before a replan) don't
|
|
2694
|
+
* require a second DB query.
|
|
2695
|
+
*
|
|
2489
2696
|
* @param cards - Cards to hydrate
|
|
2490
2697
|
* @returns Cards with tags populated
|
|
2491
2698
|
*/
|
|
@@ -2493,14 +2700,128 @@ var init_Pipeline = __esm({
|
|
|
2493
2700
|
if (cards.length === 0) {
|
|
2494
2701
|
return cards;
|
|
2495
2702
|
}
|
|
2496
|
-
const
|
|
2497
|
-
const
|
|
2703
|
+
const uncachedIds = [];
|
|
2704
|
+
for (const card of cards) {
|
|
2705
|
+
if (!this._tagCache.has(card.cardId)) {
|
|
2706
|
+
uncachedIds.push(card.cardId);
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
if (uncachedIds.length > 0) {
|
|
2710
|
+
const freshTags = await this.course.getAppliedTagsBatch(uncachedIds);
|
|
2711
|
+
for (const [cardId, tags] of freshTags) {
|
|
2712
|
+
this._tagCache.set(cardId, tags);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
const tagsByCard = /* @__PURE__ */ new Map();
|
|
2716
|
+
for (const card of cards) {
|
|
2717
|
+
tagsByCard.set(card.cardId, this._tagCache.get(card.cardId) ?? []);
|
|
2718
|
+
}
|
|
2498
2719
|
logTagHydration(cards, tagsByCard);
|
|
2499
2720
|
return cards.map((card) => ({
|
|
2500
2721
|
...card,
|
|
2501
|
-
tags:
|
|
2722
|
+
tags: this._tagCache.get(card.cardId) ?? []
|
|
2502
2723
|
}));
|
|
2503
2724
|
}
|
|
2725
|
+
// ---------------------------------------------------------------------------
|
|
2726
|
+
// Ephemeral hints application
|
|
2727
|
+
// ---------------------------------------------------------------------------
|
|
2728
|
+
/**
|
|
2729
|
+
* Apply one-shot replan hints to the post-filter card set.
|
|
2730
|
+
*
|
|
2731
|
+
* Order of operations:
|
|
2732
|
+
* 1. Exclude (remove unwanted cards)
|
|
2733
|
+
* 2. Boost (multiply scores)
|
|
2734
|
+
* 3. Require (inject must-have cards from the full pre-filter pool)
|
|
2735
|
+
*
|
|
2736
|
+
* @param cards - Post-filter cards (score > 0)
|
|
2737
|
+
* @param hints - The ephemeral hints to apply
|
|
2738
|
+
* @param allCards - Full pre-filter card pool (for require injection)
|
|
2739
|
+
*/
|
|
2740
|
+
applyHints(cards, hints, allCards) {
|
|
2741
|
+
const beforeCount = cards.length;
|
|
2742
|
+
if (hints.excludeCards?.length) {
|
|
2743
|
+
cards = cards.filter(
|
|
2744
|
+
(c) => !hints.excludeCards.some((pat) => globMatch(c.cardId, pat))
|
|
2745
|
+
);
|
|
2746
|
+
}
|
|
2747
|
+
if (hints.excludeTags?.length) {
|
|
2748
|
+
cards = cards.filter(
|
|
2749
|
+
(c) => !hints.excludeTags.some((pat) => cardMatchesTagPattern(c, pat))
|
|
2750
|
+
);
|
|
2751
|
+
}
|
|
2752
|
+
if (hints.boostTags) {
|
|
2753
|
+
for (const [pattern, factor] of Object.entries(hints.boostTags)) {
|
|
2754
|
+
for (const card of cards) {
|
|
2755
|
+
if (cardMatchesTagPattern(card, pattern)) {
|
|
2756
|
+
card.score *= factor;
|
|
2757
|
+
card.provenance.push({
|
|
2758
|
+
strategy: "ephemeralHint",
|
|
2759
|
+
strategyId: "ephemeral-hint",
|
|
2760
|
+
strategyName: "Replan Hint",
|
|
2761
|
+
action: "boosted",
|
|
2762
|
+
score: card.score,
|
|
2763
|
+
reason: `boostTag ${pattern} \xD7${factor}`
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
if (hints.boostCards) {
|
|
2770
|
+
for (const [pattern, factor] of Object.entries(hints.boostCards)) {
|
|
2771
|
+
for (const card of cards) {
|
|
2772
|
+
if (globMatch(card.cardId, pattern)) {
|
|
2773
|
+
card.score *= factor;
|
|
2774
|
+
card.provenance.push({
|
|
2775
|
+
strategy: "ephemeralHint",
|
|
2776
|
+
strategyId: "ephemeral-hint",
|
|
2777
|
+
strategyName: "Replan Hint",
|
|
2778
|
+
action: "boosted",
|
|
2779
|
+
score: card.score,
|
|
2780
|
+
reason: `boostCard ${pattern} \xD7${factor}`
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
const cardIds = new Set(cards.map((c) => c.cardId));
|
|
2787
|
+
const inject = (card, reason) => {
|
|
2788
|
+
if (!cardIds.has(card.cardId)) {
|
|
2789
|
+
const floorScore = Math.max(card.score, 1);
|
|
2790
|
+
cards.push({
|
|
2791
|
+
...card,
|
|
2792
|
+
score: floorScore,
|
|
2793
|
+
provenance: [
|
|
2794
|
+
...card.provenance,
|
|
2795
|
+
{
|
|
2796
|
+
strategy: "ephemeralHint",
|
|
2797
|
+
strategyId: "ephemeral-hint",
|
|
2798
|
+
strategyName: "Replan Hint",
|
|
2799
|
+
action: "boosted",
|
|
2800
|
+
score: floorScore,
|
|
2801
|
+
reason
|
|
2802
|
+
}
|
|
2803
|
+
]
|
|
2804
|
+
});
|
|
2805
|
+
cardIds.add(card.cardId);
|
|
2806
|
+
}
|
|
2807
|
+
};
|
|
2808
|
+
if (hints.requireCards?.length) {
|
|
2809
|
+
for (const pattern of hints.requireCards) {
|
|
2810
|
+
for (const card of allCards) {
|
|
2811
|
+
if (globMatch(card.cardId, pattern)) inject(card, `requireCard ${pattern}`);
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
if (hints.requireTags?.length) {
|
|
2816
|
+
for (const pattern of hints.requireTags) {
|
|
2817
|
+
for (const card of allCards) {
|
|
2818
|
+
if (cardMatchesTagPattern(card, pattern)) inject(card, `requireTag ${pattern}`);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
logger.info(`[Pipeline] Hints applied: ${beforeCount} \u2192 ${cards.length} cards`);
|
|
2823
|
+
return cards;
|
|
2824
|
+
}
|
|
2504
2825
|
/**
|
|
2505
2826
|
* Build shared context for generator and filters.
|
|
2506
2827
|
*
|
|
@@ -2518,7 +2839,10 @@ var init_Pipeline = __esm({
|
|
|
2518
2839
|
} catch (e) {
|
|
2519
2840
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
2520
2841
|
}
|
|
2521
|
-
|
|
2842
|
+
if (!this._cachedOrchestration) {
|
|
2843
|
+
this._cachedOrchestration = await createOrchestrationContext(this.user, this.course);
|
|
2844
|
+
}
|
|
2845
|
+
const orchestration = this._cachedOrchestration;
|
|
2522
2846
|
return {
|
|
2523
2847
|
user: this.user,
|
|
2524
2848
|
course: this.course,
|
|
@@ -2562,6 +2886,87 @@ var init_Pipeline = __esm({
|
|
|
2562
2886
|
}
|
|
2563
2887
|
return [...new Set(ids)];
|
|
2564
2888
|
}
|
|
2889
|
+
// ---------------------------------------------------------------------------
|
|
2890
|
+
// Card-space diagnostic
|
|
2891
|
+
// ---------------------------------------------------------------------------
|
|
2892
|
+
/**
|
|
2893
|
+
* Scan every card in the course through the filter chain and report
|
|
2894
|
+
* how many are "well indicated" (score >= threshold) for the current user.
|
|
2895
|
+
*
|
|
2896
|
+
* Also reports how many well-indicated cards the user has NOT yet encountered.
|
|
2897
|
+
*
|
|
2898
|
+
* Exposed via `window.skuilder.pipeline.diagnoseCardSpace()`.
|
|
2899
|
+
*/
|
|
2900
|
+
async diagnoseCardSpace(opts) {
|
|
2901
|
+
const THRESHOLD = opts?.threshold ?? 0.1;
|
|
2902
|
+
const t0 = performance.now();
|
|
2903
|
+
const allCardIds = await this.course.getAllCardIds();
|
|
2904
|
+
let cards = allCardIds.map((cardId) => ({
|
|
2905
|
+
cardId,
|
|
2906
|
+
courseId: this.course.getCourseID(),
|
|
2907
|
+
score: 1,
|
|
2908
|
+
provenance: []
|
|
2909
|
+
}));
|
|
2910
|
+
cards = await this.hydrateTags(cards);
|
|
2911
|
+
const context = await this.buildContext();
|
|
2912
|
+
const filterBreakdown = [];
|
|
2913
|
+
for (const filter of this.filters) {
|
|
2914
|
+
cards = await filter.transform(cards, context);
|
|
2915
|
+
const wi = cards.filter((c) => c.score >= THRESHOLD).length;
|
|
2916
|
+
filterBreakdown.push({ name: filter.name, wellIndicated: wi });
|
|
2917
|
+
}
|
|
2918
|
+
const wellIndicated = cards.filter((c) => c.score >= THRESHOLD);
|
|
2919
|
+
const wellIndicatedIds = new Set(wellIndicated.map((c) => c.cardId));
|
|
2920
|
+
let encounteredIds;
|
|
2921
|
+
try {
|
|
2922
|
+
const courseId = this.course.getCourseID();
|
|
2923
|
+
const seenCards = await this.user.getSeenCards(courseId);
|
|
2924
|
+
encounteredIds = new Set(seenCards);
|
|
2925
|
+
} catch {
|
|
2926
|
+
encounteredIds = /* @__PURE__ */ new Set();
|
|
2927
|
+
}
|
|
2928
|
+
const wellIndicatedNew = wellIndicated.filter((c) => !encounteredIds.has(c.cardId));
|
|
2929
|
+
const byType = /* @__PURE__ */ new Map();
|
|
2930
|
+
for (const card of cards) {
|
|
2931
|
+
const type = card.cardId.split("-")[1] || "unknown";
|
|
2932
|
+
if (!byType.has(type)) {
|
|
2933
|
+
byType.set(type, { total: 0, wellIndicated: 0, new: 0 });
|
|
2934
|
+
}
|
|
2935
|
+
const entry = byType.get(type);
|
|
2936
|
+
entry.total++;
|
|
2937
|
+
if (card.score >= THRESHOLD) {
|
|
2938
|
+
entry.wellIndicated++;
|
|
2939
|
+
if (!encounteredIds.has(card.cardId)) entry.new++;
|
|
2940
|
+
}
|
|
2941
|
+
}
|
|
2942
|
+
const elapsed = performance.now() - t0;
|
|
2943
|
+
const result = {
|
|
2944
|
+
totalCards: allCardIds.length,
|
|
2945
|
+
threshold: THRESHOLD,
|
|
2946
|
+
wellIndicated: wellIndicatedIds.size,
|
|
2947
|
+
encountered: encounteredIds.size,
|
|
2948
|
+
wellIndicatedNew: wellIndicatedNew.length,
|
|
2949
|
+
byType: Object.fromEntries(byType),
|
|
2950
|
+
filterBreakdown,
|
|
2951
|
+
elapsedMs: Math.round(elapsed)
|
|
2952
|
+
};
|
|
2953
|
+
logger.info(`[Pipeline:diagnose] Card space scan (${result.elapsedMs}ms):`);
|
|
2954
|
+
logger.info(`[Pipeline:diagnose] Total cards: ${result.totalCards}`);
|
|
2955
|
+
logger.info(`[Pipeline:diagnose] Well-indicated (score >= ${THRESHOLD}): ${result.wellIndicated}`);
|
|
2956
|
+
logger.info(`[Pipeline:diagnose] Encountered: ${result.encountered}`);
|
|
2957
|
+
logger.info(`[Pipeline:diagnose] Well-indicated & new: ${result.wellIndicatedNew}`);
|
|
2958
|
+
logger.info(`[Pipeline:diagnose] By type:`);
|
|
2959
|
+
for (const [type, counts] of byType) {
|
|
2960
|
+
logger.info(
|
|
2961
|
+
`[Pipeline:diagnose] ${type}: ${counts.wellIndicated}/${counts.total} well-indicated, ${counts.new} new`
|
|
2962
|
+
);
|
|
2963
|
+
}
|
|
2964
|
+
logger.info(`[Pipeline:diagnose] After each filter:`);
|
|
2965
|
+
for (const fb of filterBreakdown) {
|
|
2966
|
+
logger.info(`[Pipeline:diagnose] ${fb.name}: ${fb.wellIndicated} well-indicated`);
|
|
2967
|
+
}
|
|
2968
|
+
return result;
|
|
2969
|
+
}
|
|
2565
2970
|
};
|
|
2566
2971
|
}
|
|
2567
2972
|
});
|
|
@@ -2666,23 +3071,25 @@ var init_PipelineAssembler = __esm({
|
|
|
2666
3071
|
warnings.push(`Unknown strategy type '${s.implementingClass}', skipping: ${s.name}`);
|
|
2667
3072
|
}
|
|
2668
3073
|
}
|
|
3074
|
+
const courseId = course.getCourseID();
|
|
3075
|
+
const hasElo = generatorStrategies.some((s) => s.implementingClass === "elo" /* ELO */);
|
|
3076
|
+
const hasSrs = generatorStrategies.some((s) => s.implementingClass === "srs" /* SRS */);
|
|
3077
|
+
if (!hasElo) {
|
|
3078
|
+
logger.debug("[PipelineAssembler] No ELO generator configured, adding default");
|
|
3079
|
+
generatorStrategies.push(createDefaultEloStrategy(courseId));
|
|
3080
|
+
}
|
|
3081
|
+
if (!hasSrs) {
|
|
3082
|
+
logger.debug("[PipelineAssembler] No SRS generator configured, adding default");
|
|
3083
|
+
generatorStrategies.push(createDefaultSrsStrategy(courseId));
|
|
3084
|
+
}
|
|
2669
3085
|
if (generatorStrategies.length === 0) {
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
} else {
|
|
2678
|
-
warnings.push("No generator strategy found");
|
|
2679
|
-
return {
|
|
2680
|
-
pipeline: null,
|
|
2681
|
-
generatorStrategies: [],
|
|
2682
|
-
filterStrategies: [],
|
|
2683
|
-
warnings
|
|
2684
|
-
};
|
|
2685
|
-
}
|
|
3086
|
+
warnings.push("No generator strategy found");
|
|
3087
|
+
return {
|
|
3088
|
+
pipeline: null,
|
|
3089
|
+
generatorStrategies: [],
|
|
3090
|
+
filterStrategies: [],
|
|
3091
|
+
warnings
|
|
3092
|
+
};
|
|
2686
3093
|
}
|
|
2687
3094
|
let generator;
|
|
2688
3095
|
if (generatorStrategies.length === 1) {
|
|
@@ -2760,6 +3167,7 @@ var init_3 = __esm({
|
|
|
2760
3167
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
2761
3168
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
2762
3169
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
3170
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
2763
3171
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
2764
3172
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
2765
3173
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
@@ -2808,8 +3216,10 @@ async function initializeNavigatorRegistry() {
|
|
|
2808
3216
|
Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
2809
3217
|
Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
2810
3218
|
]);
|
|
3219
|
+
const prescribedModule = await Promise.resolve().then(() => (init_prescribed(), prescribed_exports));
|
|
2811
3220
|
registerNavigator("elo", eloModule.default);
|
|
2812
3221
|
registerNavigator("srs", srsModule.default);
|
|
3222
|
+
registerNavigator("prescribed", prescribedModule.default);
|
|
2813
3223
|
const [
|
|
2814
3224
|
hierarchyModule,
|
|
2815
3225
|
interferenceModule,
|
|
@@ -2864,6 +3274,7 @@ var init_navigators = __esm({
|
|
|
2864
3274
|
Navigators = /* @__PURE__ */ ((Navigators2) => {
|
|
2865
3275
|
Navigators2["ELO"] = "elo";
|
|
2866
3276
|
Navigators2["SRS"] = "srs";
|
|
3277
|
+
Navigators2["PRESCRIBED"] = "prescribed";
|
|
2867
3278
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
2868
3279
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
2869
3280
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
@@ -2878,6 +3289,7 @@ var init_navigators = __esm({
|
|
|
2878
3289
|
NavigatorRoles = {
|
|
2879
3290
|
["elo" /* ELO */]: "generator" /* GENERATOR */,
|
|
2880
3291
|
["srs" /* SRS */]: "generator" /* GENERATOR */,
|
|
3292
|
+
["prescribed" /* PRESCRIBED */]: "generator" /* GENERATOR */,
|
|
2881
3293
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
2882
3294
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
2883
3295
|
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
@@ -3042,6 +3454,12 @@ var init_navigators = __esm({
|
|
|
3042
3454
|
async getWeightedCards(_limit) {
|
|
3043
3455
|
throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
|
|
3044
3456
|
}
|
|
3457
|
+
/**
|
|
3458
|
+
* Set ephemeral hints for the next pipeline run.
|
|
3459
|
+
* No-op for non-Pipeline navigators. Pipeline overrides this.
|
|
3460
|
+
*/
|
|
3461
|
+
setEphemeralHints(_hints) {
|
|
3462
|
+
}
|
|
3045
3463
|
};
|
|
3046
3464
|
}
|
|
3047
3465
|
});
|
|
@@ -3095,6 +3513,16 @@ var init_adminDB2 = __esm({
|
|
|
3095
3513
|
}
|
|
3096
3514
|
});
|
|
3097
3515
|
|
|
3516
|
+
// src/impl/couch/CourseSyncService.ts
|
|
3517
|
+
var init_CourseSyncService = __esm({
|
|
3518
|
+
"src/impl/couch/CourseSyncService.ts"() {
|
|
3519
|
+
"use strict";
|
|
3520
|
+
init_pouchdb_setup();
|
|
3521
|
+
init_couch();
|
|
3522
|
+
init_logger();
|
|
3523
|
+
}
|
|
3524
|
+
});
|
|
3525
|
+
|
|
3098
3526
|
// src/impl/couch/auth.ts
|
|
3099
3527
|
import fetch2 from "cross-fetch";
|
|
3100
3528
|
var init_auth = __esm({
|
|
@@ -3156,6 +3584,7 @@ var init_couch = __esm({
|
|
|
3156
3584
|
init_classroomDB2();
|
|
3157
3585
|
init_courseAPI();
|
|
3158
3586
|
init_courseDB();
|
|
3587
|
+
init_CourseSyncService();
|
|
3159
3588
|
init_CouchDBSyncStrategy();
|
|
3160
3589
|
isBrowser = typeof window !== "undefined";
|
|
3161
3590
|
if (isBrowser) {
|
|
@@ -3455,6 +3884,9 @@ Currently logged-in as ${this._username}.`
|
|
|
3455
3884
|
const id = row.id;
|
|
3456
3885
|
return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
|
|
3457
3886
|
id.startsWith(DocTypePrefixes["SCHEDULED_CARD" /* SCHEDULED_CARD */]) || // Scheduled reviews
|
|
3887
|
+
id.startsWith(DocTypePrefixes["STRATEGY_STATE" /* STRATEGY_STATE */]) || // Strategy state (user prefs, progression)
|
|
3888
|
+
id.startsWith(DocTypePrefixes["USER_OUTCOME" /* USER_OUTCOME */]) || // Evolutionary orchestration outcomes
|
|
3889
|
+
id.startsWith(DocTypePrefixes["STRATEGY_LEARNING_STATE" /* STRATEGY_LEARNING_STATE */]) || // Strategy learning state
|
|
3458
3890
|
id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
3459
3891
|
id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
3460
3892
|
id === _BaseUser.DOC_IDS.CONFIG;
|
|
@@ -5307,6 +5739,10 @@ var init_courseDB3 = __esm({
|
|
|
5307
5739
|
}
|
|
5308
5740
|
return tagsByCard;
|
|
5309
5741
|
}
|
|
5742
|
+
async getAllCardIds() {
|
|
5743
|
+
const tagsIndex = await this.unpacker.getTagsIndex();
|
|
5744
|
+
return Object.keys(tagsIndex.byCard);
|
|
5745
|
+
}
|
|
5310
5746
|
async addTagToCard(_cardId, _tagId) {
|
|
5311
5747
|
throw new Error("Cannot modify tags in static mode");
|
|
5312
5748
|
}
|