@vue-skuilder/db 0.1.31-a → 0.1.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{contentSource-BmnmvH8C.d.ts → contentSource-Bdwkvqa8.d.ts} +35 -4
- package/dist/{contentSource-DfBbaLA-.d.cts → contentSource-DF1nUbPQ.d.cts} +35 -4
- package/dist/core/index.d.cts +48 -3
- package/dist/core/index.d.ts +48 -3
- package/dist/core/index.js +587 -56
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +586 -56
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BeRXVMs5.d.cts → dataLayerProvider-BKmVoyJR.d.ts} +20 -1
- package/dist/{dataLayerProvider-CG9GfaAY.d.ts → dataLayerProvider-BQdfJuBN.d.cts} +20 -1
- package/dist/impl/couch/index.d.cts +156 -4
- package/dist/impl/couch/index.d.ts +156 -4
- package/dist/impl/couch/index.js +805 -47
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +804 -47
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +3 -2
- package/dist/impl/static/index.d.ts +3 -2
- package/dist/impl/static/index.js +542 -37
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +542 -37
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +64 -3
- package/dist/index.d.ts +64 -3
- package/dist/index.js +1040 -90
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1030 -81
- package/dist/index.mjs.map +1 -1
- package/docs/navigators-architecture.md +64 -5
- package/package.json +3 -3
- package/src/core/interfaces/contentSource.ts +6 -0
- package/src/core/interfaces/courseDB.ts +6 -0
- package/src/core/interfaces/dataLayerProvider.ts +20 -0
- package/src/core/navigators/Pipeline.ts +414 -9
- package/src/core/navigators/PipelineAssembler.ts +23 -18
- package/src/core/navigators/PipelineDebugger.ts +115 -1
- package/src/core/navigators/filters/hierarchyDefinition.ts +78 -8
- package/src/core/navigators/generators/prescribed.ts +95 -0
- package/src/core/navigators/index.ts +55 -10
- package/src/impl/common/BaseUserDB.ts +4 -1
- package/src/impl/couch/CourseSyncService.ts +356 -0
- package/src/impl/couch/PouchDataLayerProvider.ts +21 -1
- package/src/impl/couch/courseDB.ts +60 -13
- package/src/impl/couch/index.ts +1 -0
- package/src/impl/static/courseDB.ts +5 -0
- package/src/study/ItemQueue.ts +42 -0
- package/src/study/SessionController.ts +195 -22
- package/src/study/SpacedRepetition.ts +7 -2
- package/tests/core/navigators/Pipeline.test.ts +1 -1
- package/tests/core/navigators/PipelineAssembler.test.ts +15 -14
|
@@ -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,11 +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";
|
|
600
|
+
init_navigators();
|
|
595
601
|
init_logger();
|
|
602
|
+
_activePipeline = null;
|
|
596
603
|
MAX_RUNS = 10;
|
|
597
604
|
runHistory = [];
|
|
598
605
|
pipelineDebugAPI = {
|
|
@@ -734,6 +741,81 @@ var init_PipelineDebugger = __esm({
|
|
|
734
741
|
runHistory.length = 0;
|
|
735
742
|
logger.info("[Pipeline Debug] Run history cleared.");
|
|
736
743
|
},
|
|
744
|
+
/**
|
|
745
|
+
* Show the navigator registry: all registered classes and their roles.
|
|
746
|
+
*
|
|
747
|
+
* Useful for verifying that consumer-defined navigators were registered
|
|
748
|
+
* before pipeline assembly.
|
|
749
|
+
*/
|
|
750
|
+
showRegistry() {
|
|
751
|
+
const names = getRegisteredNavigatorNames();
|
|
752
|
+
if (names.length === 0) {
|
|
753
|
+
logger.info("[Pipeline Debug] Navigator registry is empty.");
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
console.group("\u{1F4E6} Navigator Registry");
|
|
757
|
+
console.table(
|
|
758
|
+
names.map((name) => {
|
|
759
|
+
const registryRole = getRegisteredNavigatorRole(name);
|
|
760
|
+
const builtinRole = NavigatorRoles[name];
|
|
761
|
+
const effectiveRole = builtinRole || registryRole || "\u26A0\uFE0F NONE";
|
|
762
|
+
const source = builtinRole ? "built-in" : registryRole ? "consumer" : "unclassified";
|
|
763
|
+
return {
|
|
764
|
+
name,
|
|
765
|
+
role: effectiveRole,
|
|
766
|
+
source,
|
|
767
|
+
isGenerator: isGenerator(name),
|
|
768
|
+
isFilter: isFilter(name)
|
|
769
|
+
};
|
|
770
|
+
})
|
|
771
|
+
);
|
|
772
|
+
console.groupEnd();
|
|
773
|
+
},
|
|
774
|
+
/**
|
|
775
|
+
* Show strategy documents from the last pipeline run and how they mapped
|
|
776
|
+
* to the registry.
|
|
777
|
+
*
|
|
778
|
+
* If no runs are captured yet, falls back to showing just the registry.
|
|
779
|
+
*/
|
|
780
|
+
showStrategies() {
|
|
781
|
+
this.showRegistry();
|
|
782
|
+
if (runHistory.length === 0) {
|
|
783
|
+
logger.info("[Pipeline Debug] No pipeline runs captured yet \u2014 cannot show strategy doc mapping.");
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
const run = runHistory[0];
|
|
787
|
+
console.group("\u{1F50C} Pipeline Strategy Mapping (last run)");
|
|
788
|
+
logger.info(`Generator: ${run.generatorName}`);
|
|
789
|
+
if (run.generators && run.generators.length > 0) {
|
|
790
|
+
for (const g of run.generators) {
|
|
791
|
+
logger.info(` \u{1F4E5} ${g.name}: ${g.cardCount} cards (${g.newCount} new, ${g.reviewCount} reviews)`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
if (run.filters.length > 0) {
|
|
795
|
+
logger.info("Filters:");
|
|
796
|
+
for (const f of run.filters) {
|
|
797
|
+
logger.info(` \u{1F538} ${f.name}: \u2191${f.boosted} \u2193${f.penalized} =${f.passed} \u2715${f.removed}`);
|
|
798
|
+
}
|
|
799
|
+
} else {
|
|
800
|
+
logger.info("Filters: (none)");
|
|
801
|
+
}
|
|
802
|
+
console.groupEnd();
|
|
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
|
+
},
|
|
737
819
|
/**
|
|
738
820
|
* Show help.
|
|
739
821
|
*/
|
|
@@ -746,6 +828,9 @@ Commands:
|
|
|
746
828
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
747
829
|
.showCard(cardId) Show provenance trail for a specific card
|
|
748
830
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
831
|
+
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
832
|
+
.showRegistry() Show navigator registry (classes + roles)
|
|
833
|
+
.showStrategies() Show registry + strategy mapping from last run
|
|
749
834
|
.listRuns() List all captured runs in table format
|
|
750
835
|
.export() Export run history as JSON for bug reports
|
|
751
836
|
.clear() Clear run history
|
|
@@ -755,7 +840,7 @@ Commands:
|
|
|
755
840
|
Example:
|
|
756
841
|
window.skuilder.pipeline.showLastRun()
|
|
757
842
|
window.skuilder.pipeline.showRun(1)
|
|
758
|
-
window.skuilder.pipeline.
|
|
843
|
+
await window.skuilder.pipeline.diagnoseCardSpace()
|
|
759
844
|
`);
|
|
760
845
|
}
|
|
761
846
|
};
|
|
@@ -1050,6 +1135,69 @@ var init_generators = __esm({
|
|
|
1050
1135
|
}
|
|
1051
1136
|
});
|
|
1052
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
|
+
|
|
1053
1201
|
// src/core/navigators/generators/srs.ts
|
|
1054
1202
|
var srs_exports = {};
|
|
1055
1203
|
__export(srs_exports, {
|
|
@@ -1244,6 +1392,7 @@ var init_ = __esm({
|
|
|
1244
1392
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
1245
1393
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
1246
1394
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
1395
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
1247
1396
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
1248
1397
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports))
|
|
1249
1398
|
});
|
|
@@ -1444,6 +1593,8 @@ var init_hierarchyDefinition = __esm({
|
|
|
1444
1593
|
if (userTagElo.count < minCount) return false;
|
|
1445
1594
|
if (prereq.masteryThreshold?.minElo !== void 0) {
|
|
1446
1595
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
1596
|
+
} else if (prereq.masteryThreshold?.minCount !== void 0) {
|
|
1597
|
+
return true;
|
|
1447
1598
|
} else {
|
|
1448
1599
|
return userTagElo.score >= userGlobalElo;
|
|
1449
1600
|
}
|
|
@@ -1519,14 +1670,38 @@ var init_hierarchyDefinition = __esm({
|
|
|
1519
1670
|
};
|
|
1520
1671
|
}
|
|
1521
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
|
+
}
|
|
1522
1693
|
/**
|
|
1523
1694
|
* CardFilter.transform implementation.
|
|
1524
1695
|
*
|
|
1525
|
-
*
|
|
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
|
|
1526
1700
|
*/
|
|
1527
1701
|
async transform(cards, context) {
|
|
1528
1702
|
const masteredTags = await this.getMasteredTags(context);
|
|
1529
1703
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
1704
|
+
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
1530
1705
|
const gated = [];
|
|
1531
1706
|
for (const card of cards) {
|
|
1532
1707
|
const { isUnlocked, reason } = await this.checkCardUnlock(
|
|
@@ -1535,9 +1710,27 @@ var init_hierarchyDefinition = __esm({
|
|
|
1535
1710
|
unlockedTags,
|
|
1536
1711
|
masteredTags
|
|
1537
1712
|
);
|
|
1538
|
-
const LOCKED_PENALTY = 0.
|
|
1539
|
-
|
|
1540
|
-
|
|
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
|
+
}
|
|
1541
1734
|
gated.push({
|
|
1542
1735
|
...card,
|
|
1543
1736
|
score: finalScore,
|
|
@@ -1549,7 +1742,7 @@ var init_hierarchyDefinition = __esm({
|
|
|
1549
1742
|
strategyId: this.strategyId || "NAVIGATION_STRATEGY-hierarchy",
|
|
1550
1743
|
action,
|
|
1551
1744
|
score: finalScore,
|
|
1552
|
-
reason
|
|
1745
|
+
reason: finalReason
|
|
1553
1746
|
}
|
|
1554
1747
|
]
|
|
1555
1748
|
});
|
|
@@ -2238,6 +2431,18 @@ __export(Pipeline_exports, {
|
|
|
2238
2431
|
Pipeline: () => Pipeline
|
|
2239
2432
|
});
|
|
2240
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
|
+
}
|
|
2241
2446
|
function logPipelineConfig(generator, filters) {
|
|
2242
2447
|
const filterList = filters.length > 0 ? "\n - " + filters.map((f) => f.name).join("\n - ") : " none";
|
|
2243
2448
|
logger.info(
|
|
@@ -2272,6 +2477,21 @@ function logExecutionSummary(generatorName, generatedCount, filterCount, finalCo
|
|
|
2272
2477
|
\u{1F4A1} Inspect: window.skuilder.pipeline`
|
|
2273
2478
|
);
|
|
2274
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
|
+
}
|
|
2275
2495
|
function logCardProvenance(cards, maxCards = 3) {
|
|
2276
2496
|
const cardsToLog = cards.slice(0, maxCards);
|
|
2277
2497
|
logger.debug(`[Pipeline] Provenance for top ${cardsToLog.length} cards:`);
|
|
@@ -2286,7 +2506,7 @@ function logCardProvenance(cards, maxCards = 3) {
|
|
|
2286
2506
|
}
|
|
2287
2507
|
}
|
|
2288
2508
|
}
|
|
2289
|
-
var Pipeline;
|
|
2509
|
+
var VERBOSE_RESULTS, Pipeline;
|
|
2290
2510
|
var init_Pipeline = __esm({
|
|
2291
2511
|
"src/core/navigators/Pipeline.ts"() {
|
|
2292
2512
|
"use strict";
|
|
@@ -2294,9 +2514,31 @@ var init_Pipeline = __esm({
|
|
|
2294
2514
|
init_logger();
|
|
2295
2515
|
init_orchestration();
|
|
2296
2516
|
init_PipelineDebugger();
|
|
2517
|
+
VERBOSE_RESULTS = true;
|
|
2297
2518
|
Pipeline = class extends ContentNavigator {
|
|
2298
2519
|
generator;
|
|
2299
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;
|
|
2300
2542
|
/**
|
|
2301
2543
|
* Create a new pipeline.
|
|
2302
2544
|
*
|
|
@@ -2317,6 +2559,17 @@ var init_Pipeline = __esm({
|
|
|
2317
2559
|
logger.error(`[pipeline] Failed to lookup courseCfg: ${e}`);
|
|
2318
2560
|
});
|
|
2319
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)}`);
|
|
2320
2573
|
}
|
|
2321
2574
|
/**
|
|
2322
2575
|
* Get weighted cards by running generator and applying filters.
|
|
@@ -2333,13 +2586,15 @@ var init_Pipeline = __esm({
|
|
|
2333
2586
|
* @returns Cards sorted by score descending
|
|
2334
2587
|
*/
|
|
2335
2588
|
async getWeightedCards(limit) {
|
|
2589
|
+
const t0 = performance.now();
|
|
2336
2590
|
const context = await this.buildContext();
|
|
2337
|
-
const
|
|
2338
|
-
const fetchLimit =
|
|
2591
|
+
const tContext = performance.now();
|
|
2592
|
+
const fetchLimit = 500;
|
|
2339
2593
|
logger.debug(
|
|
2340
2594
|
`[Pipeline] Fetching ${fetchLimit} candidates from generator '${this.generator.name}'`
|
|
2341
2595
|
);
|
|
2342
2596
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
2597
|
+
const tGenerate = performance.now();
|
|
2343
2598
|
const generatedCount = cards.length;
|
|
2344
2599
|
let generatorSummaries;
|
|
2345
2600
|
if (this.generator.generators) {
|
|
@@ -2368,6 +2623,7 @@ var init_Pipeline = __esm({
|
|
|
2368
2623
|
}
|
|
2369
2624
|
logger.debug(`[Pipeline] Generator returned ${generatedCount} candidates`);
|
|
2370
2625
|
cards = await this.hydrateTags(cards);
|
|
2626
|
+
const tHydrate = performance.now();
|
|
2371
2627
|
const allCardsBeforeFiltering = [...cards];
|
|
2372
2628
|
const filterImpacts = [];
|
|
2373
2629
|
for (const filter of this.filters) {
|
|
@@ -2386,8 +2642,17 @@ var init_Pipeline = __esm({
|
|
|
2386
2642
|
logger.debug(`[Pipeline] Filter '${filter.name}': ${beforeScores.size} \u2192 ${cards.length} cards (\u2191${boosted} \u2193${penalized} =${passed})`);
|
|
2387
2643
|
}
|
|
2388
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
|
+
}
|
|
2389
2650
|
cards.sort((a, b) => b.score - a.score);
|
|
2651
|
+
const tFilter = performance.now();
|
|
2390
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
|
+
);
|
|
2391
2656
|
const topScores = result.slice(0, 3).map((c) => c.score);
|
|
2392
2657
|
logExecutionSummary(
|
|
2393
2658
|
this.generator.name,
|
|
@@ -2397,6 +2662,7 @@ var init_Pipeline = __esm({
|
|
|
2397
2662
|
topScores,
|
|
2398
2663
|
filterImpacts
|
|
2399
2664
|
);
|
|
2665
|
+
logResultCards(result);
|
|
2400
2666
|
logCardProvenance(result, 3);
|
|
2401
2667
|
try {
|
|
2402
2668
|
const courseName = await this.course?.getCourseConfig().then((c) => c.name).catch(() => void 0);
|
|
@@ -2423,6 +2689,10 @@ var init_Pipeline = __esm({
|
|
|
2423
2689
|
* to the WeightedCard objects. Filters can then use card.tags instead of
|
|
2424
2690
|
* making individual getAppliedTags() calls.
|
|
2425
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
|
+
*
|
|
2426
2696
|
* @param cards - Cards to hydrate
|
|
2427
2697
|
* @returns Cards with tags populated
|
|
2428
2698
|
*/
|
|
@@ -2430,14 +2700,128 @@ var init_Pipeline = __esm({
|
|
|
2430
2700
|
if (cards.length === 0) {
|
|
2431
2701
|
return cards;
|
|
2432
2702
|
}
|
|
2433
|
-
const
|
|
2434
|
-
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
|
+
}
|
|
2435
2719
|
logTagHydration(cards, tagsByCard);
|
|
2436
2720
|
return cards.map((card) => ({
|
|
2437
2721
|
...card,
|
|
2438
|
-
tags:
|
|
2722
|
+
tags: this._tagCache.get(card.cardId) ?? []
|
|
2439
2723
|
}));
|
|
2440
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
|
+
}
|
|
2441
2825
|
/**
|
|
2442
2826
|
* Build shared context for generator and filters.
|
|
2443
2827
|
*
|
|
@@ -2455,7 +2839,10 @@ var init_Pipeline = __esm({
|
|
|
2455
2839
|
} catch (e) {
|
|
2456
2840
|
logger.debug(`[Pipeline] Could not get user ELO, using default: ${e}`);
|
|
2457
2841
|
}
|
|
2458
|
-
|
|
2842
|
+
if (!this._cachedOrchestration) {
|
|
2843
|
+
this._cachedOrchestration = await createOrchestrationContext(this.user, this.course);
|
|
2844
|
+
}
|
|
2845
|
+
const orchestration = this._cachedOrchestration;
|
|
2459
2846
|
return {
|
|
2460
2847
|
user: this.user,
|
|
2461
2848
|
course: this.course,
|
|
@@ -2499,6 +2886,87 @@ var init_Pipeline = __esm({
|
|
|
2499
2886
|
}
|
|
2500
2887
|
return [...new Set(ids)];
|
|
2501
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
|
+
}
|
|
2502
2970
|
};
|
|
2503
2971
|
}
|
|
2504
2972
|
});
|
|
@@ -2603,23 +3071,25 @@ var init_PipelineAssembler = __esm({
|
|
|
2603
3071
|
warnings.push(`Unknown strategy type '${s.implementingClass}', skipping: ${s.name}`);
|
|
2604
3072
|
}
|
|
2605
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
|
+
}
|
|
2606
3085
|
if (generatorStrategies.length === 0) {
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
} else {
|
|
2615
|
-
warnings.push("No generator strategy found");
|
|
2616
|
-
return {
|
|
2617
|
-
pipeline: null,
|
|
2618
|
-
generatorStrategies: [],
|
|
2619
|
-
filterStrategies: [],
|
|
2620
|
-
warnings
|
|
2621
|
-
};
|
|
2622
|
-
}
|
|
3086
|
+
warnings.push("No generator strategy found");
|
|
3087
|
+
return {
|
|
3088
|
+
pipeline: null,
|
|
3089
|
+
generatorStrategies: [],
|
|
3090
|
+
filterStrategies: [],
|
|
3091
|
+
warnings
|
|
3092
|
+
};
|
|
2623
3093
|
}
|
|
2624
3094
|
let generator;
|
|
2625
3095
|
if (generatorStrategies.length === 1) {
|
|
@@ -2697,6 +3167,7 @@ var init_3 = __esm({
|
|
|
2697
3167
|
"./generators/CompositeGenerator.ts": () => Promise.resolve().then(() => (init_CompositeGenerator(), CompositeGenerator_exports)),
|
|
2698
3168
|
"./generators/elo.ts": () => Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
2699
3169
|
"./generators/index.ts": () => Promise.resolve().then(() => (init_generators(), generators_exports)),
|
|
3170
|
+
"./generators/prescribed.ts": () => Promise.resolve().then(() => (init_prescribed(), prescribed_exports)),
|
|
2700
3171
|
"./generators/srs.ts": () => Promise.resolve().then(() => (init_srs(), srs_exports)),
|
|
2701
3172
|
"./generators/types.ts": () => Promise.resolve().then(() => (init_types(), types_exports)),
|
|
2702
3173
|
"./index.ts": () => Promise.resolve().then(() => (init_navigators(), navigators_exports))
|
|
@@ -2714,6 +3185,7 @@ __export(navigators_exports, {
|
|
|
2714
3185
|
getCardOrigin: () => getCardOrigin,
|
|
2715
3186
|
getRegisteredNavigator: () => getRegisteredNavigator,
|
|
2716
3187
|
getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
|
|
3188
|
+
getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
|
|
2717
3189
|
hasRegisteredNavigator: () => hasRegisteredNavigator,
|
|
2718
3190
|
initializeNavigatorRegistry: () => initializeNavigatorRegistry,
|
|
2719
3191
|
isFilter: () => isFilter,
|
|
@@ -2722,16 +3194,19 @@ __export(navigators_exports, {
|
|
|
2722
3194
|
pipelineDebugAPI: () => pipelineDebugAPI,
|
|
2723
3195
|
registerNavigator: () => registerNavigator
|
|
2724
3196
|
});
|
|
2725
|
-
function registerNavigator(implementingClass, constructor) {
|
|
2726
|
-
navigatorRegistry.set(implementingClass, constructor);
|
|
2727
|
-
logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}`);
|
|
3197
|
+
function registerNavigator(implementingClass, constructor, role) {
|
|
3198
|
+
navigatorRegistry.set(implementingClass, { constructor, role });
|
|
3199
|
+
logger.debug(`[NavigatorRegistry] Registered: ${implementingClass}${role ? ` (${role})` : ""}`);
|
|
2728
3200
|
}
|
|
2729
3201
|
function getRegisteredNavigator(implementingClass) {
|
|
2730
|
-
return navigatorRegistry.get(implementingClass);
|
|
3202
|
+
return navigatorRegistry.get(implementingClass)?.constructor;
|
|
2731
3203
|
}
|
|
2732
3204
|
function hasRegisteredNavigator(implementingClass) {
|
|
2733
3205
|
return navigatorRegistry.has(implementingClass);
|
|
2734
3206
|
}
|
|
3207
|
+
function getRegisteredNavigatorRole(implementingClass) {
|
|
3208
|
+
return navigatorRegistry.get(implementingClass)?.role;
|
|
3209
|
+
}
|
|
2735
3210
|
function getRegisteredNavigatorNames() {
|
|
2736
3211
|
return Array.from(navigatorRegistry.keys());
|
|
2737
3212
|
}
|
|
@@ -2741,8 +3216,10 @@ async function initializeNavigatorRegistry() {
|
|
|
2741
3216
|
Promise.resolve().then(() => (init_elo(), elo_exports)),
|
|
2742
3217
|
Promise.resolve().then(() => (init_srs(), srs_exports))
|
|
2743
3218
|
]);
|
|
3219
|
+
const prescribedModule = await Promise.resolve().then(() => (init_prescribed(), prescribed_exports));
|
|
2744
3220
|
registerNavigator("elo", eloModule.default);
|
|
2745
3221
|
registerNavigator("srs", srsModule.default);
|
|
3222
|
+
registerNavigator("prescribed", prescribedModule.default);
|
|
2746
3223
|
const [
|
|
2747
3224
|
hierarchyModule,
|
|
2748
3225
|
interferenceModule,
|
|
@@ -2777,10 +3254,12 @@ function getCardOrigin(card) {
|
|
|
2777
3254
|
return "new";
|
|
2778
3255
|
}
|
|
2779
3256
|
function isGenerator(impl) {
|
|
2780
|
-
|
|
3257
|
+
if (NavigatorRoles[impl] === "generator" /* GENERATOR */) return true;
|
|
3258
|
+
return getRegisteredNavigatorRole(impl) === "generator" /* GENERATOR */;
|
|
2781
3259
|
}
|
|
2782
3260
|
function isFilter(impl) {
|
|
2783
|
-
|
|
3261
|
+
if (NavigatorRoles[impl] === "filter" /* FILTER */) return true;
|
|
3262
|
+
return getRegisteredNavigatorRole(impl) === "filter" /* FILTER */;
|
|
2784
3263
|
}
|
|
2785
3264
|
var navigatorRegistry, Navigators, NavigatorRole, NavigatorRoles, ContentNavigator;
|
|
2786
3265
|
var init_navigators = __esm({
|
|
@@ -2795,6 +3274,7 @@ var init_navigators = __esm({
|
|
|
2795
3274
|
Navigators = /* @__PURE__ */ ((Navigators2) => {
|
|
2796
3275
|
Navigators2["ELO"] = "elo";
|
|
2797
3276
|
Navigators2["SRS"] = "srs";
|
|
3277
|
+
Navigators2["PRESCRIBED"] = "prescribed";
|
|
2798
3278
|
Navigators2["HIERARCHY"] = "hierarchyDefinition";
|
|
2799
3279
|
Navigators2["INTERFERENCE"] = "interferenceMitigator";
|
|
2800
3280
|
Navigators2["RELATIVE_PRIORITY"] = "relativePriority";
|
|
@@ -2809,6 +3289,7 @@ var init_navigators = __esm({
|
|
|
2809
3289
|
NavigatorRoles = {
|
|
2810
3290
|
["elo" /* ELO */]: "generator" /* GENERATOR */,
|
|
2811
3291
|
["srs" /* SRS */]: "generator" /* GENERATOR */,
|
|
3292
|
+
["prescribed" /* PRESCRIBED */]: "generator" /* GENERATOR */,
|
|
2812
3293
|
["hierarchyDefinition" /* HIERARCHY */]: "filter" /* FILTER */,
|
|
2813
3294
|
["interferenceMitigator" /* INTERFERENCE */]: "filter" /* FILTER */,
|
|
2814
3295
|
["relativePriority" /* RELATIVE_PRIORITY */]: "filter" /* FILTER */,
|
|
@@ -2973,6 +3454,12 @@ var init_navigators = __esm({
|
|
|
2973
3454
|
async getWeightedCards(_limit) {
|
|
2974
3455
|
throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
|
|
2975
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
|
+
}
|
|
2976
3463
|
};
|
|
2977
3464
|
}
|
|
2978
3465
|
});
|
|
@@ -3026,6 +3513,16 @@ var init_adminDB2 = __esm({
|
|
|
3026
3513
|
}
|
|
3027
3514
|
});
|
|
3028
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
|
+
|
|
3029
3526
|
// src/impl/couch/auth.ts
|
|
3030
3527
|
import fetch2 from "cross-fetch";
|
|
3031
3528
|
var init_auth = __esm({
|
|
@@ -3087,6 +3584,7 @@ var init_couch = __esm({
|
|
|
3087
3584
|
init_classroomDB2();
|
|
3088
3585
|
init_courseAPI();
|
|
3089
3586
|
init_courseDB();
|
|
3587
|
+
init_CourseSyncService();
|
|
3090
3588
|
init_CouchDBSyncStrategy();
|
|
3091
3589
|
isBrowser = typeof window !== "undefined";
|
|
3092
3590
|
if (isBrowser) {
|
|
@@ -3386,6 +3884,9 @@ Currently logged-in as ${this._username}.`
|
|
|
3386
3884
|
const id = row.id;
|
|
3387
3885
|
return id.startsWith(DocTypePrefixes["CARDRECORD" /* CARDRECORD */]) || // Card interaction history
|
|
3388
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
|
|
3389
3890
|
id === _BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
3390
3891
|
id === _BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
3391
3892
|
id === _BaseUser.DOC_IDS.CONFIG;
|
|
@@ -5238,6 +5739,10 @@ var init_courseDB3 = __esm({
|
|
|
5238
5739
|
}
|
|
5239
5740
|
return tagsByCard;
|
|
5240
5741
|
}
|
|
5742
|
+
async getAllCardIds() {
|
|
5743
|
+
const tagsIndex = await this.unpacker.getTagsIndex();
|
|
5744
|
+
return Object.keys(tagsIndex.byCard);
|
|
5745
|
+
}
|
|
5241
5746
|
async addTagToCard(_cardId, _tagId) {
|
|
5242
5747
|
throw new Error("Cannot modify tags in static mode");
|
|
5243
5748
|
}
|