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