@vue-skuilder/db 0.2.16 → 0.2.18

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 (44) hide show
  1. package/dist/{SyncStrategy-CyATpyLQ.d.cts → SyncStrategy-BJMZq3WO.d.cts} +17 -0
  2. package/dist/{SyncStrategy-CyATpyLQ.d.ts → SyncStrategy-BJMZq3WO.d.ts} +17 -0
  3. package/dist/{contentSource-Brz42x7n.d.cts → contentSource-CBqZCoGU.d.cts} +85 -1
  4. package/dist/{contentSource-B1p-vdz7.d.ts → contentSource-CudEz5Tm.d.ts} +85 -1
  5. package/dist/core/index.d.cts +51 -4
  6. package/dist/core/index.d.ts +51 -4
  7. package/dist/core/index.js +258 -7
  8. package/dist/core/index.js.map +1 -1
  9. package/dist/core/index.mjs +254 -7
  10. package/dist/core/index.mjs.map +1 -1
  11. package/dist/{dataLayerProvider-CpwpT1rM.d.cts → dataLayerProvider-BBA8tJNx.d.cts} +1 -1
  12. package/dist/{dataLayerProvider-BWayUIoK.d.ts → dataLayerProvider-C9WgkBzR.d.ts} +1 -1
  13. package/dist/impl/couch/index.d.cts +16 -3
  14. package/dist/impl/couch/index.d.ts +16 -3
  15. package/dist/impl/couch/index.js +284 -9
  16. package/dist/impl/couch/index.js.map +1 -1
  17. package/dist/impl/couch/index.mjs +284 -9
  18. package/dist/impl/couch/index.mjs.map +1 -1
  19. package/dist/impl/static/index.d.cts +3 -3
  20. package/dist/impl/static/index.d.ts +3 -3
  21. package/dist/impl/static/index.js +250 -7
  22. package/dist/impl/static/index.js.map +1 -1
  23. package/dist/impl/static/index.mjs +250 -7
  24. package/dist/impl/static/index.mjs.map +1 -1
  25. package/dist/index.d.cts +8 -6
  26. package/dist/index.d.ts +8 -6
  27. package/dist/index.js +354 -10
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +350 -10
  30. package/dist/index.mjs.map +1 -1
  31. package/package.json +4 -3
  32. package/src/core/index.ts +1 -0
  33. package/src/core/interfaces/userDB.ts +11 -0
  34. package/src/core/navigators/StrategyPressureDebugger.ts +76 -0
  35. package/src/core/navigators/generators/prescribed.ts +122 -7
  36. package/src/core/navigators/index.ts +12 -0
  37. package/src/core/types/hydration.ts +78 -0
  38. package/src/impl/common/BaseUserDB.ts +202 -2
  39. package/src/impl/common/SyncStrategy.ts +19 -0
  40. package/src/impl/couch/CouchDBSyncStrategy.ts +54 -4
  41. package/src/study/SessionController.ts +7 -1
  42. package/src/study/SessionDebugger.ts +2 -0
  43. package/src/study/SessionOverlay.ts +113 -0
  44. package/tests/impl/hydration.test.ts +281 -0
@@ -1667,6 +1667,30 @@ var init_SrsDebugger = __esm({
1667
1667
  }
1668
1668
  });
1669
1669
 
1670
+ // src/core/navigators/StrategyPressureDebugger.ts
1671
+ var StrategyPressureDebugger_exports = {};
1672
+ __export(StrategyPressureDebugger_exports, {
1673
+ captureStrategyPressure: () => captureStrategyPressure,
1674
+ clearStrategyPressureDebug: () => clearStrategyPressureDebug,
1675
+ getStrategyPressureDebug: () => getStrategyPressureDebug
1676
+ });
1677
+ function captureStrategyPressure(snapshot) {
1678
+ snapshots2.set(`${snapshot.source}:${snapshot.courseId}`, snapshot);
1679
+ }
1680
+ function getStrategyPressureDebug() {
1681
+ return [...snapshots2.values()].sort((a, b) => b.timestamp - a.timestamp);
1682
+ }
1683
+ function clearStrategyPressureDebug() {
1684
+ snapshots2.clear();
1685
+ }
1686
+ var snapshots2;
1687
+ var init_StrategyPressureDebugger = __esm({
1688
+ "src/core/navigators/StrategyPressureDebugger.ts"() {
1689
+ "use strict";
1690
+ snapshots2 = /* @__PURE__ */ new Map();
1691
+ }
1692
+ });
1693
+
1670
1694
  // src/core/navigators/generators/CompositeGenerator.ts
1671
1695
  var CompositeGenerator_exports = {};
1672
1696
  __export(CompositeGenerator_exports, {
@@ -2031,6 +2055,13 @@ function shuffleInPlace(arr) {
2031
2055
  [arr[i], arr[j]] = [arr[j], arr[i]];
2032
2056
  }
2033
2057
  }
2058
+ function formatAge(ms) {
2059
+ const minutes = Math.max(0, Math.round(ms / 6e4));
2060
+ if (minutes < 60) return `${minutes}m`;
2061
+ const hours = Math.round(minutes / 60);
2062
+ if (hours < 24) return `${hours}h`;
2063
+ return `${Math.round(hours / 24)}d`;
2064
+ }
2034
2065
  function pickTopByScore(cards, limit) {
2035
2066
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
2036
2067
  }
@@ -2039,6 +2070,7 @@ var init_prescribed = __esm({
2039
2070
  "src/core/navigators/generators/prescribed.ts"() {
2040
2071
  "use strict";
2041
2072
  init_navigators();
2073
+ init_StrategyPressureDebugger();
2042
2074
  init_logger();
2043
2075
  DEFAULT_FRESHNESS_WINDOW = 3;
2044
2076
  DEFAULT_MAX_DIRECT_PER_RUN = 3;
@@ -2115,6 +2147,7 @@ var init_prescribed = __esm({
2115
2147
  const groupRuntimes = [];
2116
2148
  const priorPracticeDebt = progress.practiceDebt ?? {};
2117
2149
  const nextPracticeDebt = {};
2150
+ const practiceDebtsByGroup = /* @__PURE__ */ new Map();
2118
2151
  for (const group of this.config.groups) {
2119
2152
  const runtime = this.buildGroupRuntimeState({
2120
2153
  group,
@@ -2163,7 +2196,7 @@ var init_prescribed = __esm({
2163
2196
  courseId,
2164
2197
  emittedIds
2165
2198
  );
2166
- const practiceCards = this.buildPracticeCards({
2199
+ const practice = this.buildPracticeCards({
2167
2200
  group,
2168
2201
  courseId,
2169
2202
  emittedIds,
@@ -2176,7 +2209,8 @@ var init_prescribed = __esm({
2176
2209
  priorPracticeDebt,
2177
2210
  nextPracticeDebt
2178
2211
  });
2179
- emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
2212
+ practiceDebtsByGroup.set(group.id, practice.debts);
2213
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practice.cards);
2180
2214
  }
2181
2215
  nextState.practiceDebt = nextPracticeDebt;
2182
2216
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
@@ -2192,6 +2226,7 @@ var init_prescribed = __esm({
2192
2226
  } else {
2193
2227
  logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
2194
2228
  }
2229
+ this.capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints);
2195
2230
  if (emitted.length === 0) {
2196
2231
  logger.info(
2197
2232
  "[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
@@ -2253,6 +2288,60 @@ var init_prescribed = __esm({
2253
2288
  supportTags: [...supportTags].sort()
2254
2289
  };
2255
2290
  }
2291
+ /**
2292
+ * Translate this run's per-group runtimes and practice debts into gauges on
2293
+ * the generic strategy-pressure debug channel (rendered by the live session
2294
+ * overlay's "strategy backpressure" section).
2295
+ */
2296
+ capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints) {
2297
+ const now = Date.now();
2298
+ const gauges = [];
2299
+ for (const runtime of groupRuntimes) {
2300
+ const group = runtime.group;
2301
+ const window2 = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
2302
+ gauges.push({
2303
+ id: `group:${group.id}:target`,
2304
+ label: `${group.id} targets`,
2305
+ multiplier: runtime.pressureMultiplier,
2306
+ max: MAX_TARGET_MULTIPLIER,
2307
+ detail: `${runtime.pendingTargets.length} pending (${runtime.surfaceableTargets.length} surfaceable, ${runtime.blockedTargets.length} blocked) \xB7 sinceSurfaced ${runtime.sessionsSinceSurfaced}/${window2}`,
2308
+ items: runtime.blockedTargets.map((cardId) => ({ label: cardId, value: "blocked" }))
2309
+ });
2310
+ if (runtime.blockedTargets.length > 0) {
2311
+ const mode = runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support" : "boost-only";
2312
+ gauges.push({
2313
+ id: `group:${group.id}:support`,
2314
+ label: `${group.id} support`,
2315
+ multiplier: runtime.supportMultiplier,
2316
+ max: MAX_SUPPORT_MULTIPLIER,
2317
+ detail: `mode=${mode} \xB7 ${runtime.supportTags.length} support tag(s)`,
2318
+ items: runtime.supportTags.map((tag) => ({ label: tag }))
2319
+ });
2320
+ }
2321
+ const debts = practiceDebtsByGroup.get(group.id) ?? [];
2322
+ if (debts.length > 0) {
2323
+ gauges.push({
2324
+ id: `group:${group.id}:practice-debt`,
2325
+ label: `${group.id} practice debt`,
2326
+ multiplier: Math.max(...debts.map((d) => d.multiplier)),
2327
+ max: MAX_PRACTICE_MULTIPLIER,
2328
+ detail: `${debts.length} under-practiced skill(s)`,
2329
+ items: debts.map((d) => ({
2330
+ label: d.tag,
2331
+ value: `\xD7${d.multiplier.toFixed(2)} \xB7 open ${formatAge(now - new Date(d.firstOwedAt).getTime())}`
2332
+ }))
2333
+ });
2334
+ }
2335
+ }
2336
+ captureStrategyPressure({
2337
+ source: "prescribed",
2338
+ courseId,
2339
+ gauges,
2340
+ topScore: emitted.length > 0 ? Math.max(...emitted.map((c) => c.score)) : null,
2341
+ hintsLabel: hints?._label,
2342
+ timestamp: now
2343
+ });
2344
+ }
2256
2345
  parseConfig(serializedData) {
2257
2346
  try {
2258
2347
  const parsed = JSON.parse(serializedData);
@@ -2409,6 +2498,7 @@ var init_prescribed = __esm({
2409
2498
  supportTags: [...supportTags],
2410
2499
  pressureMultiplier,
2411
2500
  supportMultiplier,
2501
+ sessionsSinceSurfaced,
2412
2502
  debugVersion: PRESCRIBED_DEBUG_VERSION
2413
2503
  };
2414
2504
  }
@@ -2541,13 +2631,13 @@ var init_prescribed = __esm({
2541
2631
  nextPracticeDebt
2542
2632
  } = args;
2543
2633
  const patterns = group.practiceTagPatterns ?? [];
2544
- if (patterns.length === 0) return [];
2634
+ if (patterns.length === 0) return { cards: [], debts: [] };
2545
2635
  const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
2546
2636
  const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
2547
2637
  const practiceTags = [...cardsByTag.keys()].filter(
2548
2638
  (tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
2549
2639
  );
2550
- if (practiceTags.length === 0) return [];
2640
+ if (practiceTags.length === 0) return { cards: [], debts: [] };
2551
2641
  const now = Date.now();
2552
2642
  const DAY_MS = 24 * 60 * 60 * 1e3;
2553
2643
  const tagMultiplier = /* @__PURE__ */ new Map();
@@ -2562,6 +2652,11 @@ var init_prescribed = __esm({
2562
2652
  );
2563
2653
  tagMultiplier.set(tag, mult);
2564
2654
  }
2655
+ const debts = practiceTags.map((tag) => ({
2656
+ tag,
2657
+ multiplier: tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT,
2658
+ firstOwedAt: nextPracticeDebt[tag]
2659
+ })).sort((a, b) => b.multiplier - a.multiplier || a.tag.localeCompare(b.tag));
2565
2660
  const practiceCardIds = this.findDiscoveredSupportCards({
2566
2661
  supportTags: practiceTags,
2567
2662
  cardsByTag,
@@ -2570,7 +2665,7 @@ var init_prescribed = __esm({
2570
2665
  excludedIds: emittedIds,
2571
2666
  limit: maxPractice
2572
2667
  });
2573
- if (practiceCardIds.length === 0) return [];
2668
+ if (practiceCardIds.length === 0) return { cards: [], debts };
2574
2669
  logger.info(
2575
2670
  `[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
2576
2671
  );
@@ -2600,7 +2695,7 @@ var init_prescribed = __esm({
2600
2695
  ]
2601
2696
  });
2602
2697
  }
2603
- return cards;
2698
+ return { cards, debts };
2604
2699
  }
2605
2700
  /**
2606
2701
  * True for a skill that was *gated and is now reached*: it has at least one
@@ -5277,6 +5372,7 @@ var init_3 = __esm({
5277
5372
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
5278
5373
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
5279
5374
  "./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
5375
+ "./StrategyPressureDebugger.ts": () => Promise.resolve().then(() => (init_StrategyPressureDebugger(), StrategyPressureDebugger_exports)),
5280
5376
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
5281
5377
  "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
5282
5378
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
@@ -5309,7 +5405,9 @@ __export(navigators_exports, {
5309
5405
  NavigatorRole: () => NavigatorRole,
5310
5406
  NavigatorRoles: () => NavigatorRoles,
5311
5407
  Navigators: () => Navigators,
5408
+ captureStrategyPressure: () => captureStrategyPressure,
5312
5409
  clearSrsBacklogDebug: () => clearSrsBacklogDebug,
5410
+ clearStrategyPressureDebug: () => clearStrategyPressureDebug,
5313
5411
  diversityRerank: () => diversityRerank,
5314
5412
  getActivePipeline: () => getActivePipeline,
5315
5413
  getCardOrigin: () => getCardOrigin,
@@ -5317,6 +5415,7 @@ __export(navigators_exports, {
5317
5415
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
5318
5416
  getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
5319
5417
  getSrsBacklogDebug: () => getSrsBacklogDebug,
5418
+ getStrategyPressureDebug: () => getStrategyPressureDebug,
5320
5419
  hasRegisteredNavigator: () => hasRegisteredNavigator,
5321
5420
  initializeNavigatorRegistry: () => initializeNavigatorRegistry,
5322
5421
  isFilter: () => isFilter,
@@ -5399,6 +5498,7 @@ var init_navigators = __esm({
5399
5498
  init_diversityRerank();
5400
5499
  init_PipelineDebugger();
5401
5500
  init_SrsDebugger();
5501
+ init_StrategyPressureDebugger();
5402
5502
  init_logger();
5403
5503
  init_();
5404
5504
  init_2();
@@ -6667,6 +6767,19 @@ var init_couch = __esm({
6667
6767
  });
6668
6768
 
6669
6769
  // src/impl/common/BaseUserDB.ts
6770
+ function withTimeout(p, ms, label) {
6771
+ let timer;
6772
+ return Promise.race([
6773
+ p,
6774
+ new Promise((_resolve, reject) => {
6775
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
6776
+ })
6777
+ ]).finally(() => {
6778
+ if (timer !== void 0) {
6779
+ clearTimeout(timer);
6780
+ }
6781
+ });
6782
+ }
6670
6783
  async function getOrCreateClassroomRegistrationsDoc(user) {
6671
6784
  let ret;
6672
6785
  try {
@@ -6759,7 +6872,7 @@ async function dropUserFromClassroom(user, classID) {
6759
6872
  async function getUserClassrooms(user) {
6760
6873
  return getOrCreateClassroomRegistrationsDoc(user);
6761
6874
  }
6762
- var import_common12, import_moment6, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
6875
+ var import_common12, import_moment6, log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
6763
6876
  var init_BaseUserDB = __esm({
6764
6877
  "src/impl/common/BaseUserDB.ts"() {
6765
6878
  "use strict";
@@ -6776,6 +6889,7 @@ var init_BaseUserDB = __esm({
6776
6889
  log3 = (s) => {
6777
6890
  logger.info(s);
6778
6891
  };
6892
+ HYDRATION_TIMEOUT_MS = 15e3;
6779
6893
  BaseUser = class _BaseUser {
6780
6894
  static _instance;
6781
6895
  static _initialized = false;
@@ -6804,6 +6918,29 @@ var init_BaseUserDB = __esm({
6804
6918
  writeDB;
6805
6919
  // Database to use for write operations (local-first approach)
6806
6920
  updateQueue;
6921
+ _hydration = { state: "not-required" };
6922
+ /**
6923
+ * How far the local mirror can be trusted. See {@link UserHydrationStatus}.
6924
+ *
6925
+ * Consumers that would otherwise read a 404 as "new user" — onboarding
6926
+ * gates, first-run experiences, progress dashboards — should check for
6927
+ * `failed` and present a retry affordance rather than an empty state.
6928
+ */
6929
+ hydrationStatus() {
6930
+ return { ...this._hydration };
6931
+ }
6932
+ /**
6933
+ * Whether a missing document may be treated as genuinely absent, allowing
6934
+ * callers to create it with defaults.
6935
+ *
6936
+ * False only when hydration failed or is still running: a 404 then may just
6937
+ * mean "not pulled down yet", and materializing a default would both lie to
6938
+ * the user and, once live sync starts, push a rev-1 document that loses to
6939
+ * the real one — silently discarding it.
6940
+ */
6941
+ canMaterializeDefaults() {
6942
+ return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
6943
+ }
6807
6944
  async createAccount(username, password) {
6808
6945
  if (!this.syncStrategy.canCreateAccount()) {
6809
6946
  throw new Error("Account creation not supported by current sync strategy");
@@ -6921,6 +7058,11 @@ Currently logged-in as ${this._username}.`
6921
7058
  } catch (e) {
6922
7059
  const err = e;
6923
7060
  if (err.status === 404) {
7061
+ if (!this.canMaterializeDefaults()) {
7062
+ throw new Error(
7063
+ `Cannot read course registrations for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}'). Refusing to create an empty registration doc \u2014 that would report an existing user as unregistered, and then lose to their real document once sync resumes.`
7064
+ );
7065
+ }
6924
7066
  await this.localDB.put({
6925
7067
  _id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
6926
7068
  courses: [],
@@ -7163,6 +7305,11 @@ Currently logged-in as ${this._username}.`
7163
7305
  } catch (e) {
7164
7306
  const err = e;
7165
7307
  if (err.name && err.name === "not_found") {
7308
+ if (!this.canMaterializeDefaults()) {
7309
+ throw new Error(
7310
+ `Cannot read config for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}'). Refusing to write a default config over what may be an existing one.`
7311
+ );
7312
+ }
7166
7313
  await this.localDB.put(defaultConfig);
7167
7314
  return this.getConfig();
7168
7315
  } else {
@@ -7236,7 +7383,9 @@ Currently logged-in as ${this._username}.`
7236
7383
  _BaseUser._initialized = true;
7237
7384
  return;
7238
7385
  }
7386
+ this.syncStrategy.stopSync?.();
7239
7387
  this.setDBandQ();
7388
+ await this.hydrateLocalMirror();
7240
7389
  this.syncStrategy.startSync(this.localDB, this.remoteDB);
7241
7390
  this.applyDesignDocs().catch((error) => {
7242
7391
  log3(`Error in applyDesignDocs background task: ${error}`);
@@ -7252,6 +7401,85 @@ Currently logged-in as ${this._username}.`
7252
7401
  });
7253
7402
  _BaseUser._initialized = true;
7254
7403
  }
7404
+ /**
7405
+ * Pull this account's documents into the local mirror, if that hasn't
7406
+ * happened on this device before.
7407
+ *
7408
+ * Runs between setDBandQ() and startSync() — after the handles exist, before
7409
+ * anything can observe an empty local DB or replicate one upward.
7410
+ *
7411
+ * Never throws: a failure is recorded as `failed` state and reported through
7412
+ * hydrationStatus(), because throwing here would abort init() and leave
7413
+ * BaseUser._initialized false forever (BaseUser.instance() polls it with no
7414
+ * ceiling). Callers decide what a failure means for them.
7415
+ */
7416
+ async hydrateLocalMirror() {
7417
+ if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
7418
+ this._hydration = { state: "not-required" };
7419
+ return;
7420
+ }
7421
+ if (await this.hasHydrationMarker()) {
7422
+ this._hydration = { state: "stale" };
7423
+ return;
7424
+ }
7425
+ this._hydration = { state: "hydrating" };
7426
+ const start = Date.now();
7427
+ try {
7428
+ const { docsWritten } = await withTimeout(
7429
+ this.syncStrategy.hydrate(this.localDB, this.remoteDB),
7430
+ HYDRATION_TIMEOUT_MS,
7431
+ `Hydration of local mirror for ${this._username}`
7432
+ );
7433
+ await this.writeHydrationMarker();
7434
+ this._hydration = {
7435
+ state: "hydrated",
7436
+ docsWritten,
7437
+ durationMs: Date.now() - start
7438
+ };
7439
+ log3(
7440
+ `Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
7441
+ );
7442
+ } catch (e) {
7443
+ this.syncStrategy.stopSync?.();
7444
+ this._hydration = {
7445
+ state: "failed",
7446
+ durationMs: Date.now() - start,
7447
+ error: e instanceof Error ? e.message : String(e)
7448
+ };
7449
+ logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
7450
+ }
7451
+ }
7452
+ /**
7453
+ * Has a full pull completed on this device for the CURRENT account?
7454
+ *
7455
+ * The marker is a `_local/` document, which never replicates — so its
7456
+ * presence describes this device's mirror and cannot arrive from elsewhere.
7457
+ */
7458
+ async hasHydrationMarker() {
7459
+ try {
7460
+ const marker = await this.localDB.get(HYDRATION_MARKER_ID);
7461
+ return marker.username === this._username;
7462
+ } catch {
7463
+ return false;
7464
+ }
7465
+ }
7466
+ async writeHydrationMarker() {
7467
+ try {
7468
+ let existingRev;
7469
+ try {
7470
+ existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
7471
+ } catch {
7472
+ }
7473
+ await this.localDB.put({
7474
+ _id: HYDRATION_MARKER_ID,
7475
+ _rev: existingRev,
7476
+ username: this._username,
7477
+ hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
7478
+ });
7479
+ } catch (e) {
7480
+ logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
7481
+ }
7482
+ }
7255
7483
  static designDocs = [
7256
7484
  {
7257
7485
  _id: "_design/reviewCards",
@@ -7588,6 +7816,11 @@ Currently logged-in as ${this._username}.`
7588
7816
  } catch (e) {
7589
7817
  const err = e;
7590
7818
  if (err.status === 404) {
7819
+ if (!this.canMaterializeDefaults()) {
7820
+ throw new Error(
7821
+ `Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
7822
+ );
7823
+ }
7591
7824
  return null;
7592
7825
  }
7593
7826
  throw e;
@@ -7936,6 +8169,15 @@ var init_userOutcome = __esm({
7936
8169
  }
7937
8170
  });
7938
8171
 
8172
+ // src/core/types/hydration.ts
8173
+ var HYDRATION_MARKER_ID;
8174
+ var init_hydration = __esm({
8175
+ "src/core/types/hydration.ts"() {
8176
+ "use strict";
8177
+ HYDRATION_MARKER_ID = "_local/hydration";
8178
+ }
8179
+ });
8180
+
7939
8181
  // src/core/bulkImport/cardProcessor.ts
7940
8182
  async function importParsedCards(parsedCards, courseDB, config) {
7941
8183
  const results = [];
@@ -8416,6 +8658,7 @@ __export(core_exports, {
8416
8658
  DocType: () => DocType,
8417
8659
  DocTypePrefixes: () => DocTypePrefixes,
8418
8660
  GuestUsername: () => GuestUsername,
8661
+ HYDRATION_MARKER_ID: () => HYDRATION_MARKER_ID,
8419
8662
  Loggable: () => Loggable,
8420
8663
  NavigatorRole: () => NavigatorRole,
8421
8664
  NavigatorRoles: () => NavigatorRoles,
@@ -8423,7 +8666,9 @@ __export(core_exports, {
8423
8666
  aggregateOutcomesForGradient: () => aggregateOutcomesForGradient,
8424
8667
  areQuestionRecords: () => areQuestionRecords,
8425
8668
  buildStrategyStateId: () => buildStrategyStateId,
8669
+ captureStrategyPressure: () => captureStrategyPressure,
8426
8670
  clearSrsBacklogDebug: () => clearSrsBacklogDebug,
8671
+ clearStrategyPressureDebug: () => clearStrategyPressureDebug,
8427
8672
  computeDeviation: () => computeDeviation,
8428
8673
  computeEffectiveWeight: () => computeEffectiveWeight,
8429
8674
  computeOutcomeSignal: () => computeOutcomeSignal,
@@ -8440,6 +8685,7 @@ __export(core_exports, {
8440
8685
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
8441
8686
  getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
8442
8687
  getSrsBacklogDebug: () => getSrsBacklogDebug,
8688
+ getStrategyPressureDebug: () => getStrategyPressureDebug,
8443
8689
  getStudySource: () => getStudySource,
8444
8690
  hasRegisteredNavigator: () => hasRegisteredNavigator,
8445
8691
  importParsedCards: () => importParsedCards,
@@ -8470,6 +8716,7 @@ var init_core = __esm({
8470
8716
  init_user();
8471
8717
  init_strategyState();
8472
8718
  init_userOutcome();
8719
+ init_hydration();
8473
8720
  init_Loggable();
8474
8721
  init_util();
8475
8722
  init_navigators();
@@ -8487,6 +8734,7 @@ init_core();
8487
8734
  DocType,
8488
8735
  DocTypePrefixes,
8489
8736
  GuestUsername,
8737
+ HYDRATION_MARKER_ID,
8490
8738
  Loggable,
8491
8739
  NavigatorRole,
8492
8740
  NavigatorRoles,
@@ -8494,7 +8742,9 @@ init_core();
8494
8742
  aggregateOutcomesForGradient,
8495
8743
  areQuestionRecords,
8496
8744
  buildStrategyStateId,
8745
+ captureStrategyPressure,
8497
8746
  clearSrsBacklogDebug,
8747
+ clearStrategyPressureDebug,
8498
8748
  computeDeviation,
8499
8749
  computeEffectiveWeight,
8500
8750
  computeOutcomeSignal,
@@ -8511,6 +8761,7 @@ init_core();
8511
8761
  getRegisteredNavigatorNames,
8512
8762
  getRegisteredNavigatorRole,
8513
8763
  getSrsBacklogDebug,
8764
+ getStrategyPressureDebug,
8514
8765
  getStudySource,
8515
8766
  hasRegisteredNavigator,
8516
8767
  importParsedCards,