@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
@@ -1644,6 +1644,30 @@ var init_SrsDebugger = __esm({
1644
1644
  }
1645
1645
  });
1646
1646
 
1647
+ // src/core/navigators/StrategyPressureDebugger.ts
1648
+ var StrategyPressureDebugger_exports = {};
1649
+ __export(StrategyPressureDebugger_exports, {
1650
+ captureStrategyPressure: () => captureStrategyPressure,
1651
+ clearStrategyPressureDebug: () => clearStrategyPressureDebug,
1652
+ getStrategyPressureDebug: () => getStrategyPressureDebug
1653
+ });
1654
+ function captureStrategyPressure(snapshot) {
1655
+ snapshots2.set(`${snapshot.source}:${snapshot.courseId}`, snapshot);
1656
+ }
1657
+ function getStrategyPressureDebug() {
1658
+ return [...snapshots2.values()].sort((a, b) => b.timestamp - a.timestamp);
1659
+ }
1660
+ function clearStrategyPressureDebug() {
1661
+ snapshots2.clear();
1662
+ }
1663
+ var snapshots2;
1664
+ var init_StrategyPressureDebugger = __esm({
1665
+ "src/core/navigators/StrategyPressureDebugger.ts"() {
1666
+ "use strict";
1667
+ snapshots2 = /* @__PURE__ */ new Map();
1668
+ }
1669
+ });
1670
+
1647
1671
  // src/core/navigators/generators/CompositeGenerator.ts
1648
1672
  var CompositeGenerator_exports = {};
1649
1673
  __export(CompositeGenerator_exports, {
@@ -2008,6 +2032,13 @@ function shuffleInPlace(arr) {
2008
2032
  [arr[i], arr[j]] = [arr[j], arr[i]];
2009
2033
  }
2010
2034
  }
2035
+ function formatAge(ms) {
2036
+ const minutes = Math.max(0, Math.round(ms / 6e4));
2037
+ if (minutes < 60) return `${minutes}m`;
2038
+ const hours = Math.round(minutes / 60);
2039
+ if (hours < 24) return `${hours}h`;
2040
+ return `${Math.round(hours / 24)}d`;
2041
+ }
2011
2042
  function pickTopByScore(cards, limit) {
2012
2043
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
2013
2044
  }
@@ -2016,6 +2047,7 @@ var init_prescribed = __esm({
2016
2047
  "src/core/navigators/generators/prescribed.ts"() {
2017
2048
  "use strict";
2018
2049
  init_navigators();
2050
+ init_StrategyPressureDebugger();
2019
2051
  init_logger();
2020
2052
  DEFAULT_FRESHNESS_WINDOW = 3;
2021
2053
  DEFAULT_MAX_DIRECT_PER_RUN = 3;
@@ -2092,6 +2124,7 @@ var init_prescribed = __esm({
2092
2124
  const groupRuntimes = [];
2093
2125
  const priorPracticeDebt = progress.practiceDebt ?? {};
2094
2126
  const nextPracticeDebt = {};
2127
+ const practiceDebtsByGroup = /* @__PURE__ */ new Map();
2095
2128
  for (const group of this.config.groups) {
2096
2129
  const runtime = this.buildGroupRuntimeState({
2097
2130
  group,
@@ -2140,7 +2173,7 @@ var init_prescribed = __esm({
2140
2173
  courseId,
2141
2174
  emittedIds
2142
2175
  );
2143
- const practiceCards = this.buildPracticeCards({
2176
+ const practice = this.buildPracticeCards({
2144
2177
  group,
2145
2178
  courseId,
2146
2179
  emittedIds,
@@ -2153,7 +2186,8 @@ var init_prescribed = __esm({
2153
2186
  priorPracticeDebt,
2154
2187
  nextPracticeDebt
2155
2188
  });
2156
- emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
2189
+ practiceDebtsByGroup.set(group.id, practice.debts);
2190
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practice.cards);
2157
2191
  }
2158
2192
  nextState.practiceDebt = nextPracticeDebt;
2159
2193
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
@@ -2169,6 +2203,7 @@ var init_prescribed = __esm({
2169
2203
  } else {
2170
2204
  logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
2171
2205
  }
2206
+ this.capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints);
2172
2207
  if (emitted.length === 0) {
2173
2208
  logger.info(
2174
2209
  "[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
@@ -2230,6 +2265,60 @@ var init_prescribed = __esm({
2230
2265
  supportTags: [...supportTags].sort()
2231
2266
  };
2232
2267
  }
2268
+ /**
2269
+ * Translate this run's per-group runtimes and practice debts into gauges on
2270
+ * the generic strategy-pressure debug channel (rendered by the live session
2271
+ * overlay's "strategy backpressure" section).
2272
+ */
2273
+ capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints) {
2274
+ const now = Date.now();
2275
+ const gauges = [];
2276
+ for (const runtime of groupRuntimes) {
2277
+ const group = runtime.group;
2278
+ const window2 = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
2279
+ gauges.push({
2280
+ id: `group:${group.id}:target`,
2281
+ label: `${group.id} targets`,
2282
+ multiplier: runtime.pressureMultiplier,
2283
+ max: MAX_TARGET_MULTIPLIER,
2284
+ detail: `${runtime.pendingTargets.length} pending (${runtime.surfaceableTargets.length} surfaceable, ${runtime.blockedTargets.length} blocked) \xB7 sinceSurfaced ${runtime.sessionsSinceSurfaced}/${window2}`,
2285
+ items: runtime.blockedTargets.map((cardId) => ({ label: cardId, value: "blocked" }))
2286
+ });
2287
+ if (runtime.blockedTargets.length > 0) {
2288
+ const mode = runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support" : "boost-only";
2289
+ gauges.push({
2290
+ id: `group:${group.id}:support`,
2291
+ label: `${group.id} support`,
2292
+ multiplier: runtime.supportMultiplier,
2293
+ max: MAX_SUPPORT_MULTIPLIER,
2294
+ detail: `mode=${mode} \xB7 ${runtime.supportTags.length} support tag(s)`,
2295
+ items: runtime.supportTags.map((tag) => ({ label: tag }))
2296
+ });
2297
+ }
2298
+ const debts = practiceDebtsByGroup.get(group.id) ?? [];
2299
+ if (debts.length > 0) {
2300
+ gauges.push({
2301
+ id: `group:${group.id}:practice-debt`,
2302
+ label: `${group.id} practice debt`,
2303
+ multiplier: Math.max(...debts.map((d) => d.multiplier)),
2304
+ max: MAX_PRACTICE_MULTIPLIER,
2305
+ detail: `${debts.length} under-practiced skill(s)`,
2306
+ items: debts.map((d) => ({
2307
+ label: d.tag,
2308
+ value: `\xD7${d.multiplier.toFixed(2)} \xB7 open ${formatAge(now - new Date(d.firstOwedAt).getTime())}`
2309
+ }))
2310
+ });
2311
+ }
2312
+ }
2313
+ captureStrategyPressure({
2314
+ source: "prescribed",
2315
+ courseId,
2316
+ gauges,
2317
+ topScore: emitted.length > 0 ? Math.max(...emitted.map((c) => c.score)) : null,
2318
+ hintsLabel: hints?._label,
2319
+ timestamp: now
2320
+ });
2321
+ }
2233
2322
  parseConfig(serializedData) {
2234
2323
  try {
2235
2324
  const parsed = JSON.parse(serializedData);
@@ -2386,6 +2475,7 @@ var init_prescribed = __esm({
2386
2475
  supportTags: [...supportTags],
2387
2476
  pressureMultiplier,
2388
2477
  supportMultiplier,
2478
+ sessionsSinceSurfaced,
2389
2479
  debugVersion: PRESCRIBED_DEBUG_VERSION
2390
2480
  };
2391
2481
  }
@@ -2518,13 +2608,13 @@ var init_prescribed = __esm({
2518
2608
  nextPracticeDebt
2519
2609
  } = args;
2520
2610
  const patterns = group.practiceTagPatterns ?? [];
2521
- if (patterns.length === 0) return [];
2611
+ if (patterns.length === 0) return { cards: [], debts: [] };
2522
2612
  const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
2523
2613
  const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
2524
2614
  const practiceTags = [...cardsByTag.keys()].filter(
2525
2615
  (tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
2526
2616
  );
2527
- if (practiceTags.length === 0) return [];
2617
+ if (practiceTags.length === 0) return { cards: [], debts: [] };
2528
2618
  const now = Date.now();
2529
2619
  const DAY_MS = 24 * 60 * 60 * 1e3;
2530
2620
  const tagMultiplier = /* @__PURE__ */ new Map();
@@ -2539,6 +2629,11 @@ var init_prescribed = __esm({
2539
2629
  );
2540
2630
  tagMultiplier.set(tag, mult);
2541
2631
  }
2632
+ const debts = practiceTags.map((tag) => ({
2633
+ tag,
2634
+ multiplier: tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT,
2635
+ firstOwedAt: nextPracticeDebt[tag]
2636
+ })).sort((a, b) => b.multiplier - a.multiplier || a.tag.localeCompare(b.tag));
2542
2637
  const practiceCardIds = this.findDiscoveredSupportCards({
2543
2638
  supportTags: practiceTags,
2544
2639
  cardsByTag,
@@ -2547,7 +2642,7 @@ var init_prescribed = __esm({
2547
2642
  excludedIds: emittedIds,
2548
2643
  limit: maxPractice
2549
2644
  });
2550
- if (practiceCardIds.length === 0) return [];
2645
+ if (practiceCardIds.length === 0) return { cards: [], debts };
2551
2646
  logger.info(
2552
2647
  `[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
2553
2648
  );
@@ -2577,7 +2672,7 @@ var init_prescribed = __esm({
2577
2672
  ]
2578
2673
  });
2579
2674
  }
2580
- return cards;
2675
+ return { cards, debts };
2581
2676
  }
2582
2677
  /**
2583
2678
  * True for a skill that was *gated and is now reached*: it has at least one
@@ -5254,6 +5349,7 @@ var init_3 = __esm({
5254
5349
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
5255
5350
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
5256
5351
  "./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
5352
+ "./StrategyPressureDebugger.ts": () => Promise.resolve().then(() => (init_StrategyPressureDebugger(), StrategyPressureDebugger_exports)),
5257
5353
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
5258
5354
  "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
5259
5355
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
@@ -5286,7 +5382,9 @@ __export(navigators_exports, {
5286
5382
  NavigatorRole: () => NavigatorRole,
5287
5383
  NavigatorRoles: () => NavigatorRoles,
5288
5384
  Navigators: () => Navigators,
5385
+ captureStrategyPressure: () => captureStrategyPressure,
5289
5386
  clearSrsBacklogDebug: () => clearSrsBacklogDebug,
5387
+ clearStrategyPressureDebug: () => clearStrategyPressureDebug,
5290
5388
  diversityRerank: () => diversityRerank,
5291
5389
  getActivePipeline: () => getActivePipeline,
5292
5390
  getCardOrigin: () => getCardOrigin,
@@ -5294,6 +5392,7 @@ __export(navigators_exports, {
5294
5392
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
5295
5393
  getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
5296
5394
  getSrsBacklogDebug: () => getSrsBacklogDebug,
5395
+ getStrategyPressureDebug: () => getStrategyPressureDebug,
5297
5396
  hasRegisteredNavigator: () => hasRegisteredNavigator,
5298
5397
  initializeNavigatorRegistry: () => initializeNavigatorRegistry,
5299
5398
  isFilter: () => isFilter,
@@ -5376,6 +5475,7 @@ var init_navigators = __esm({
5376
5475
  init_diversityRerank();
5377
5476
  init_PipelineDebugger();
5378
5477
  init_SrsDebugger();
5478
+ init_StrategyPressureDebugger();
5379
5479
  init_logger();
5380
5480
  init_();
5381
5481
  init_2();
@@ -6649,6 +6749,19 @@ var init_couch = __esm({
6649
6749
  // src/impl/common/BaseUserDB.ts
6650
6750
  import { Status as Status3 } from "@vue-skuilder/common";
6651
6751
  import moment6 from "moment";
6752
+ function withTimeout(p, ms, label) {
6753
+ let timer;
6754
+ return Promise.race([
6755
+ p,
6756
+ new Promise((_resolve, reject) => {
6757
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
6758
+ })
6759
+ ]).finally(() => {
6760
+ if (timer !== void 0) {
6761
+ clearTimeout(timer);
6762
+ }
6763
+ });
6764
+ }
6652
6765
  async function getOrCreateClassroomRegistrationsDoc(user) {
6653
6766
  let ret;
6654
6767
  try {
@@ -6741,7 +6854,7 @@ async function dropUserFromClassroom(user, classID) {
6741
6854
  async function getUserClassrooms(user) {
6742
6855
  return getOrCreateClassroomRegistrationsDoc(user);
6743
6856
  }
6744
- var log3, BaseUser, userCoursesDoc, userClassroomsDoc;
6857
+ var log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
6745
6858
  var init_BaseUserDB = __esm({
6746
6859
  "src/impl/common/BaseUserDB.ts"() {
6747
6860
  "use strict";
@@ -6756,6 +6869,7 @@ var init_BaseUserDB = __esm({
6756
6869
  log3 = (s) => {
6757
6870
  logger.info(s);
6758
6871
  };
6872
+ HYDRATION_TIMEOUT_MS = 15e3;
6759
6873
  BaseUser = class _BaseUser {
6760
6874
  static _instance;
6761
6875
  static _initialized = false;
@@ -6784,6 +6898,29 @@ var init_BaseUserDB = __esm({
6784
6898
  writeDB;
6785
6899
  // Database to use for write operations (local-first approach)
6786
6900
  updateQueue;
6901
+ _hydration = { state: "not-required" };
6902
+ /**
6903
+ * How far the local mirror can be trusted. See {@link UserHydrationStatus}.
6904
+ *
6905
+ * Consumers that would otherwise read a 404 as "new user" — onboarding
6906
+ * gates, first-run experiences, progress dashboards — should check for
6907
+ * `failed` and present a retry affordance rather than an empty state.
6908
+ */
6909
+ hydrationStatus() {
6910
+ return { ...this._hydration };
6911
+ }
6912
+ /**
6913
+ * Whether a missing document may be treated as genuinely absent, allowing
6914
+ * callers to create it with defaults.
6915
+ *
6916
+ * False only when hydration failed or is still running: a 404 then may just
6917
+ * mean "not pulled down yet", and materializing a default would both lie to
6918
+ * the user and, once live sync starts, push a rev-1 document that loses to
6919
+ * the real one — silently discarding it.
6920
+ */
6921
+ canMaterializeDefaults() {
6922
+ return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
6923
+ }
6787
6924
  async createAccount(username, password) {
6788
6925
  if (!this.syncStrategy.canCreateAccount()) {
6789
6926
  throw new Error("Account creation not supported by current sync strategy");
@@ -6901,6 +7038,11 @@ Currently logged-in as ${this._username}.`
6901
7038
  } catch (e) {
6902
7039
  const err = e;
6903
7040
  if (err.status === 404) {
7041
+ if (!this.canMaterializeDefaults()) {
7042
+ throw new Error(
7043
+ `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.`
7044
+ );
7045
+ }
6904
7046
  await this.localDB.put({
6905
7047
  _id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
6906
7048
  courses: [],
@@ -7143,6 +7285,11 @@ Currently logged-in as ${this._username}.`
7143
7285
  } catch (e) {
7144
7286
  const err = e;
7145
7287
  if (err.name && err.name === "not_found") {
7288
+ if (!this.canMaterializeDefaults()) {
7289
+ throw new Error(
7290
+ `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.`
7291
+ );
7292
+ }
7146
7293
  await this.localDB.put(defaultConfig);
7147
7294
  return this.getConfig();
7148
7295
  } else {
@@ -7216,7 +7363,9 @@ Currently logged-in as ${this._username}.`
7216
7363
  _BaseUser._initialized = true;
7217
7364
  return;
7218
7365
  }
7366
+ this.syncStrategy.stopSync?.();
7219
7367
  this.setDBandQ();
7368
+ await this.hydrateLocalMirror();
7220
7369
  this.syncStrategy.startSync(this.localDB, this.remoteDB);
7221
7370
  this.applyDesignDocs().catch((error) => {
7222
7371
  log3(`Error in applyDesignDocs background task: ${error}`);
@@ -7232,6 +7381,85 @@ Currently logged-in as ${this._username}.`
7232
7381
  });
7233
7382
  _BaseUser._initialized = true;
7234
7383
  }
7384
+ /**
7385
+ * Pull this account's documents into the local mirror, if that hasn't
7386
+ * happened on this device before.
7387
+ *
7388
+ * Runs between setDBandQ() and startSync() — after the handles exist, before
7389
+ * anything can observe an empty local DB or replicate one upward.
7390
+ *
7391
+ * Never throws: a failure is recorded as `failed` state and reported through
7392
+ * hydrationStatus(), because throwing here would abort init() and leave
7393
+ * BaseUser._initialized false forever (BaseUser.instance() polls it with no
7394
+ * ceiling). Callers decide what a failure means for them.
7395
+ */
7396
+ async hydrateLocalMirror() {
7397
+ if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
7398
+ this._hydration = { state: "not-required" };
7399
+ return;
7400
+ }
7401
+ if (await this.hasHydrationMarker()) {
7402
+ this._hydration = { state: "stale" };
7403
+ return;
7404
+ }
7405
+ this._hydration = { state: "hydrating" };
7406
+ const start = Date.now();
7407
+ try {
7408
+ const { docsWritten } = await withTimeout(
7409
+ this.syncStrategy.hydrate(this.localDB, this.remoteDB),
7410
+ HYDRATION_TIMEOUT_MS,
7411
+ `Hydration of local mirror for ${this._username}`
7412
+ );
7413
+ await this.writeHydrationMarker();
7414
+ this._hydration = {
7415
+ state: "hydrated",
7416
+ docsWritten,
7417
+ durationMs: Date.now() - start
7418
+ };
7419
+ log3(
7420
+ `Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
7421
+ );
7422
+ } catch (e) {
7423
+ this.syncStrategy.stopSync?.();
7424
+ this._hydration = {
7425
+ state: "failed",
7426
+ durationMs: Date.now() - start,
7427
+ error: e instanceof Error ? e.message : String(e)
7428
+ };
7429
+ logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
7430
+ }
7431
+ }
7432
+ /**
7433
+ * Has a full pull completed on this device for the CURRENT account?
7434
+ *
7435
+ * The marker is a `_local/` document, which never replicates — so its
7436
+ * presence describes this device's mirror and cannot arrive from elsewhere.
7437
+ */
7438
+ async hasHydrationMarker() {
7439
+ try {
7440
+ const marker = await this.localDB.get(HYDRATION_MARKER_ID);
7441
+ return marker.username === this._username;
7442
+ } catch {
7443
+ return false;
7444
+ }
7445
+ }
7446
+ async writeHydrationMarker() {
7447
+ try {
7448
+ let existingRev;
7449
+ try {
7450
+ existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
7451
+ } catch {
7452
+ }
7453
+ await this.localDB.put({
7454
+ _id: HYDRATION_MARKER_ID,
7455
+ _rev: existingRev,
7456
+ username: this._username,
7457
+ hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
7458
+ });
7459
+ } catch (e) {
7460
+ logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
7461
+ }
7462
+ }
7235
7463
  static designDocs = [
7236
7464
  {
7237
7465
  _id: "_design/reviewCards",
@@ -7568,6 +7796,11 @@ Currently logged-in as ${this._username}.`
7568
7796
  } catch (e) {
7569
7797
  const err = e;
7570
7798
  if (err.status === 404) {
7799
+ if (!this.canMaterializeDefaults()) {
7800
+ throw new Error(
7801
+ `Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
7802
+ );
7803
+ }
7571
7804
  return null;
7572
7805
  }
7573
7806
  throw e;
@@ -7915,6 +8148,15 @@ var init_userOutcome = __esm({
7915
8148
  }
7916
8149
  });
7917
8150
 
8151
+ // src/core/types/hydration.ts
8152
+ var HYDRATION_MARKER_ID;
8153
+ var init_hydration = __esm({
8154
+ "src/core/types/hydration.ts"() {
8155
+ "use strict";
8156
+ HYDRATION_MARKER_ID = "_local/hydration";
8157
+ }
8158
+ });
8159
+
7918
8160
  // src/core/bulkImport/cardProcessor.ts
7919
8161
  import { Status as Status4 } from "@vue-skuilder/common";
7920
8162
  async function importParsedCards(parsedCards, courseDB, config) {
@@ -8393,6 +8635,7 @@ var init_core = __esm({
8393
8635
  init_user();
8394
8636
  init_strategyState();
8395
8637
  init_userOutcome();
8638
+ init_hydration();
8396
8639
  init_Loggable();
8397
8640
  init_util();
8398
8641
  init_navigators();
@@ -8409,6 +8652,7 @@ export {
8409
8652
  DocType,
8410
8653
  DocTypePrefixes,
8411
8654
  GuestUsername,
8655
+ HYDRATION_MARKER_ID,
8412
8656
  Loggable,
8413
8657
  NavigatorRole,
8414
8658
  NavigatorRoles,
@@ -8416,7 +8660,9 @@ export {
8416
8660
  aggregateOutcomesForGradient,
8417
8661
  areQuestionRecords,
8418
8662
  buildStrategyStateId,
8663
+ captureStrategyPressure,
8419
8664
  clearSrsBacklogDebug,
8665
+ clearStrategyPressureDebug,
8420
8666
  computeDeviation,
8421
8667
  computeEffectiveWeight,
8422
8668
  computeOutcomeSignal,
@@ -8433,6 +8679,7 @@ export {
8433
8679
  getRegisteredNavigatorNames,
8434
8680
  getRegisteredNavigatorRole,
8435
8681
  getSrsBacklogDebug,
8682
+ getStrategyPressureDebug,
8436
8683
  getStudySource,
8437
8684
  hasRegisteredNavigator,
8438
8685
  importParsedCards,