@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
@@ -1,4 +1,4 @@
1
- import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-Brz42x7n.cjs';
1
+ import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-CBqZCoGU.cjs';
2
2
 
3
3
  /**
4
4
  * Main factory interface for data access
@@ -1,4 +1,4 @@
1
- import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-B1p-vdz7.js';
1
+ import { U as UserDBInterface, a as UserDBReader, C as CourseDBInterface, b as CoursesDBInterface, c as ClassroomDBInterface, A as AdminDBInterface } from './contentSource-CudEz5Tm.js';
2
2
 
3
3
  /**
4
4
  * Main factory interface for data access
@@ -1,10 +1,10 @@
1
1
  import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-4tlwHnXo.cjs';
2
2
  import { Moment } from 'moment';
3
- import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-Brz42x7n.cjs';
4
- export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-Brz42x7n.cjs';
3
+ import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-CBqZCoGU.cjs';
4
+ export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-CBqZCoGU.cjs';
5
5
  import * as _vue_skuilder_common from '@vue-skuilder/common';
6
6
  import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
7
- import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.cjs';
7
+ import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.cjs';
8
8
 
9
9
  type NamespacedDatashape = string;
10
10
  interface DataShape55 {
@@ -497,8 +497,21 @@ declare class CourseSyncService {
497
497
  */
498
498
  declare class CouchDBSyncStrategy implements SyncStrategy {
499
499
  private syncHandle?;
500
+ /** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
501
+ private hydrationHandle?;
500
502
  setupRemoteDB(username: string): PouchDB.Database;
501
503
  getWriteDB(username: string): PouchDB.Database;
504
+ /**
505
+ * One-shot remote → local pull. Resolves once the local mirror is caught up.
506
+ *
507
+ * Pull only: pushing here would upload whatever the local DB happens to hold
508
+ * before we know what the remote already has, which is the conflict-leaf
509
+ * problem hydration exists to avoid. Local changes go up when startSync()
510
+ * takes over.
511
+ */
512
+ hydrate(localDB: PouchDB.Database, remoteDB: PouchDB.Database): Promise<{
513
+ docsWritten: number;
514
+ }>;
502
515
  startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database): void;
503
516
  stopSync?(): void;
504
517
  canCreateAccount(): boolean;
@@ -1,10 +1,10 @@
1
1
  import { T as TagStub, a as Tag, S as SkuilderCourseData, Q as QualifiedCardID } from '../../types-legacy-4tlwHnXo.js';
2
2
  import { Moment } from 'moment';
3
- import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-B1p-vdz7.js';
4
- export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-B1p-vdz7.js';
3
+ import { A as AdminDBInterface, g as AssignedContent, h as StudyContentSource, i as StudentClassroomDBInterface, U as UserDBInterface, G as GeneratorResult, T as TeacherClassroomDBInterface, b as CoursesDBInterface, C as CourseDBInterface, d as CourseInfo, D as DataLayerResult, e as ContentNavigationStrategyData, f as ContentNavigator, R as ReplanHints, S as StudySessionItem, j as ScheduledCard } from '../../contentSource-CudEz5Tm.js';
4
+ export { q as ContentSourceID, k as StudySessionFailedItem, l as StudySessionFailedNewItem, m as StudySessionFailedReviewItem, n as StudySessionNewItem, o as StudySessionReviewItem, r as getStudySource, p as isReview } from '../../contentSource-CudEz5Tm.js';
5
5
  import * as _vue_skuilder_common from '@vue-skuilder/common';
6
6
  import { ClassroomConfig, DataShape, CourseElo, CourseConfig as CourseConfig$1 } from '@vue-skuilder/common';
7
- import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-CyATpyLQ.js';
7
+ import { S as SyncStrategy, A as AccountCreationResult, a as AuthenticationResult } from '../../SyncStrategy-BJMZq3WO.js';
8
8
 
9
9
  type NamespacedDatashape = string;
10
10
  interface DataShape55 {
@@ -497,8 +497,21 @@ declare class CourseSyncService {
497
497
  */
498
498
  declare class CouchDBSyncStrategy implements SyncStrategy {
499
499
  private syncHandle?;
500
+ /** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
501
+ private hydrationHandle?;
500
502
  setupRemoteDB(username: string): PouchDB.Database;
501
503
  getWriteDB(username: string): PouchDB.Database;
504
+ /**
505
+ * One-shot remote → local pull. Resolves once the local mirror is caught up.
506
+ *
507
+ * Pull only: pushing here would upload whatever the local DB happens to hold
508
+ * before we know what the remote already has, which is the conflict-leaf
509
+ * problem hydration exists to avoid. Local changes go up when startSync()
510
+ * takes over.
511
+ */
512
+ hydrate(localDB: PouchDB.Database, remoteDB: PouchDB.Database): Promise<{
513
+ docsWritten: number;
514
+ }>;
502
515
  startSync(localDB: PouchDB.Database, remoteDB: PouchDB.Database): void;
503
516
  stopSync?(): void;
504
517
  canCreateAccount(): boolean;
@@ -1588,6 +1588,30 @@ var init_SrsDebugger = __esm({
1588
1588
  }
1589
1589
  });
1590
1590
 
1591
+ // src/core/navigators/StrategyPressureDebugger.ts
1592
+ var StrategyPressureDebugger_exports = {};
1593
+ __export(StrategyPressureDebugger_exports, {
1594
+ captureStrategyPressure: () => captureStrategyPressure,
1595
+ clearStrategyPressureDebug: () => clearStrategyPressureDebug,
1596
+ getStrategyPressureDebug: () => getStrategyPressureDebug
1597
+ });
1598
+ function captureStrategyPressure(snapshot) {
1599
+ snapshots2.set(`${snapshot.source}:${snapshot.courseId}`, snapshot);
1600
+ }
1601
+ function getStrategyPressureDebug() {
1602
+ return [...snapshots2.values()].sort((a, b) => b.timestamp - a.timestamp);
1603
+ }
1604
+ function clearStrategyPressureDebug() {
1605
+ snapshots2.clear();
1606
+ }
1607
+ var snapshots2;
1608
+ var init_StrategyPressureDebugger = __esm({
1609
+ "src/core/navigators/StrategyPressureDebugger.ts"() {
1610
+ "use strict";
1611
+ snapshots2 = /* @__PURE__ */ new Map();
1612
+ }
1613
+ });
1614
+
1591
1615
  // src/core/navigators/generators/CompositeGenerator.ts
1592
1616
  var CompositeGenerator_exports = {};
1593
1617
  __export(CompositeGenerator_exports, {
@@ -1952,6 +1976,13 @@ function shuffleInPlace(arr) {
1952
1976
  [arr[i], arr[j]] = [arr[j], arr[i]];
1953
1977
  }
1954
1978
  }
1979
+ function formatAge(ms) {
1980
+ const minutes = Math.max(0, Math.round(ms / 6e4));
1981
+ if (minutes < 60) return `${minutes}m`;
1982
+ const hours = Math.round(minutes / 60);
1983
+ if (hours < 24) return `${hours}h`;
1984
+ return `${Math.round(hours / 24)}d`;
1985
+ }
1955
1986
  function pickTopByScore(cards, limit) {
1956
1987
  return [...cards].sort((a, b) => b.score - a.score || a.cardId.localeCompare(b.cardId)).slice(0, limit);
1957
1988
  }
@@ -1960,6 +1991,7 @@ var init_prescribed = __esm({
1960
1991
  "src/core/navigators/generators/prescribed.ts"() {
1961
1992
  "use strict";
1962
1993
  init_navigators();
1994
+ init_StrategyPressureDebugger();
1963
1995
  init_logger();
1964
1996
  DEFAULT_FRESHNESS_WINDOW = 3;
1965
1997
  DEFAULT_MAX_DIRECT_PER_RUN = 3;
@@ -2036,6 +2068,7 @@ var init_prescribed = __esm({
2036
2068
  const groupRuntimes = [];
2037
2069
  const priorPracticeDebt = progress.practiceDebt ?? {};
2038
2070
  const nextPracticeDebt = {};
2071
+ const practiceDebtsByGroup = /* @__PURE__ */ new Map();
2039
2072
  for (const group of this.config.groups) {
2040
2073
  const runtime = this.buildGroupRuntimeState({
2041
2074
  group,
@@ -2084,7 +2117,7 @@ var init_prescribed = __esm({
2084
2117
  courseId,
2085
2118
  emittedIds
2086
2119
  );
2087
- const practiceCards = this.buildPracticeCards({
2120
+ const practice = this.buildPracticeCards({
2088
2121
  group,
2089
2122
  courseId,
2090
2123
  emittedIds,
@@ -2097,7 +2130,8 @@ var init_prescribed = __esm({
2097
2130
  priorPracticeDebt,
2098
2131
  nextPracticeDebt
2099
2132
  });
2100
- emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practiceCards);
2133
+ practiceDebtsByGroup.set(group.id, practice.debts);
2134
+ emitted.push(...directCards, ...supportCards, ...discoveredSupportCards, ...practice.cards);
2101
2135
  }
2102
2136
  nextState.practiceDebt = nextPracticeDebt;
2103
2137
  const hintSummary = this.buildSupportHintSummary(groupRuntimes);
@@ -2113,6 +2147,7 @@ var init_prescribed = __esm({
2113
2147
  } else {
2114
2148
  logger.info("[Prescribed] No hints to emit (no blocked targets or no support tags)");
2115
2149
  }
2150
+ this.capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints);
2116
2151
  if (emitted.length === 0) {
2117
2152
  logger.info(
2118
2153
  "[Prescribed] 0 cards emitted (all targets blocked, authored/discovered support candidates exhausted)" + (hints ? " \u2014 boost hints emitted but may not survive filters" : "")
@@ -2174,6 +2209,60 @@ var init_prescribed = __esm({
2174
2209
  supportTags: [...supportTags].sort()
2175
2210
  };
2176
2211
  }
2212
+ /**
2213
+ * Translate this run's per-group runtimes and practice debts into gauges on
2214
+ * the generic strategy-pressure debug channel (rendered by the live session
2215
+ * overlay's "strategy backpressure" section).
2216
+ */
2217
+ capturePressureSnapshot(courseId, groupRuntimes, practiceDebtsByGroup, emitted, hints) {
2218
+ const now = Date.now();
2219
+ const gauges = [];
2220
+ for (const runtime of groupRuntimes) {
2221
+ const group = runtime.group;
2222
+ const window2 = group.freshnessWindowSessions ?? DEFAULT_FRESHNESS_WINDOW;
2223
+ gauges.push({
2224
+ id: `group:${group.id}:target`,
2225
+ label: `${group.id} targets`,
2226
+ multiplier: runtime.pressureMultiplier,
2227
+ max: MAX_TARGET_MULTIPLIER,
2228
+ detail: `${runtime.pendingTargets.length} pending (${runtime.surfaceableTargets.length} surfaceable, ${runtime.blockedTargets.length} blocked) \xB7 sinceSurfaced ${runtime.sessionsSinceSurfaced}/${window2}`,
2229
+ items: runtime.blockedTargets.map((cardId) => ({ label: cardId, value: "blocked" }))
2230
+ });
2231
+ if (runtime.blockedTargets.length > 0) {
2232
+ const mode = runtime.supportCandidates.length > 0 ? "direct-support" : runtime.discoveredSupportCandidates.length > 0 ? "inserted-support" : "boost-only";
2233
+ gauges.push({
2234
+ id: `group:${group.id}:support`,
2235
+ label: `${group.id} support`,
2236
+ multiplier: runtime.supportMultiplier,
2237
+ max: MAX_SUPPORT_MULTIPLIER,
2238
+ detail: `mode=${mode} \xB7 ${runtime.supportTags.length} support tag(s)`,
2239
+ items: runtime.supportTags.map((tag) => ({ label: tag }))
2240
+ });
2241
+ }
2242
+ const debts = practiceDebtsByGroup.get(group.id) ?? [];
2243
+ if (debts.length > 0) {
2244
+ gauges.push({
2245
+ id: `group:${group.id}:practice-debt`,
2246
+ label: `${group.id} practice debt`,
2247
+ multiplier: Math.max(...debts.map((d) => d.multiplier)),
2248
+ max: MAX_PRACTICE_MULTIPLIER,
2249
+ detail: `${debts.length} under-practiced skill(s)`,
2250
+ items: debts.map((d) => ({
2251
+ label: d.tag,
2252
+ value: `\xD7${d.multiplier.toFixed(2)} \xB7 open ${formatAge(now - new Date(d.firstOwedAt).getTime())}`
2253
+ }))
2254
+ });
2255
+ }
2256
+ }
2257
+ captureStrategyPressure({
2258
+ source: "prescribed",
2259
+ courseId,
2260
+ gauges,
2261
+ topScore: emitted.length > 0 ? Math.max(...emitted.map((c) => c.score)) : null,
2262
+ hintsLabel: hints?._label,
2263
+ timestamp: now
2264
+ });
2265
+ }
2177
2266
  parseConfig(serializedData) {
2178
2267
  try {
2179
2268
  const parsed = JSON.parse(serializedData);
@@ -2330,6 +2419,7 @@ var init_prescribed = __esm({
2330
2419
  supportTags: [...supportTags],
2331
2420
  pressureMultiplier,
2332
2421
  supportMultiplier,
2422
+ sessionsSinceSurfaced,
2333
2423
  debugVersion: PRESCRIBED_DEBUG_VERSION
2334
2424
  };
2335
2425
  }
@@ -2462,13 +2552,13 @@ var init_prescribed = __esm({
2462
2552
  nextPracticeDebt
2463
2553
  } = args;
2464
2554
  const patterns = group.practiceTagPatterns ?? [];
2465
- if (patterns.length === 0) return [];
2555
+ if (patterns.length === 0) return { cards: [], debts: [] };
2466
2556
  const practiceMinCount = group.practiceMinCount ?? DEFAULT_PRACTICE_MIN_COUNT;
2467
2557
  const maxPractice = group.maxPracticeCardsPerRun ?? DEFAULT_MAX_PRACTICE_PER_RUN;
2468
2558
  const practiceTags = [...cardsByTag.keys()].filter(
2469
2559
  (tag) => patterns.some((p) => matchesTagPattern(tag, p)) && this.isUnlockedGatedSkill(tag, hierarchyConfigs, userTagElo, userGlobalElo) && (userTagElo[tag]?.count ?? 0) < practiceMinCount
2470
2560
  );
2471
- if (practiceTags.length === 0) return [];
2561
+ if (practiceTags.length === 0) return { cards: [], debts: [] };
2472
2562
  const now = Date.now();
2473
2563
  const DAY_MS = 24 * 60 * 60 * 1e3;
2474
2564
  const tagMultiplier = /* @__PURE__ */ new Map();
@@ -2483,6 +2573,11 @@ var init_prescribed = __esm({
2483
2573
  );
2484
2574
  tagMultiplier.set(tag, mult);
2485
2575
  }
2576
+ const debts = practiceTags.map((tag) => ({
2577
+ tag,
2578
+ multiplier: tagMultiplier.get(tag) ?? PRACTICE_BASE_MULT,
2579
+ firstOwedAt: nextPracticeDebt[tag]
2580
+ })).sort((a, b) => b.multiplier - a.multiplier || a.tag.localeCompare(b.tag));
2486
2581
  const practiceCardIds = this.findDiscoveredSupportCards({
2487
2582
  supportTags: practiceTags,
2488
2583
  cardsByTag,
@@ -2491,7 +2586,7 @@ var init_prescribed = __esm({
2491
2586
  excludedIds: emittedIds,
2492
2587
  limit: maxPractice
2493
2588
  });
2494
- if (practiceCardIds.length === 0) return [];
2589
+ if (practiceCardIds.length === 0) return { cards: [], debts };
2495
2590
  logger.info(
2496
2591
  `[Prescribed] Group '${group.id}' practice: ${practiceTags.length} unlocked under-practiced skill(s), emitting ${practiceCardIds.length} drill card(s)`
2497
2592
  );
@@ -2521,7 +2616,7 @@ var init_prescribed = __esm({
2521
2616
  ]
2522
2617
  });
2523
2618
  }
2524
- return cards;
2619
+ return { cards, debts };
2525
2620
  }
2526
2621
  /**
2527
2622
  * True for a skill that was *gated and is now reached*: it has at least one
@@ -4952,6 +5047,7 @@ var init_3 = __esm({
4952
5047
  "./PipelineAssembler.ts": () => Promise.resolve().then(() => (init_PipelineAssembler(), PipelineAssembler_exports)),
4953
5048
  "./PipelineDebugger.ts": () => Promise.resolve().then(() => (init_PipelineDebugger(), PipelineDebugger_exports)),
4954
5049
  "./SrsDebugger.ts": () => Promise.resolve().then(() => (init_SrsDebugger(), SrsDebugger_exports)),
5050
+ "./StrategyPressureDebugger.ts": () => Promise.resolve().then(() => (init_StrategyPressureDebugger(), StrategyPressureDebugger_exports)),
4955
5051
  "./defaults.ts": () => Promise.resolve().then(() => (init_defaults(), defaults_exports)),
4956
5052
  "./diversityRerank.ts": () => Promise.resolve().then(() => (init_diversityRerank(), diversityRerank_exports)),
4957
5053
  "./filters/WeightedFilter.ts": () => Promise.resolve().then(() => (init_WeightedFilter(), WeightedFilter_exports)),
@@ -4984,7 +5080,9 @@ __export(navigators_exports, {
4984
5080
  NavigatorRole: () => NavigatorRole,
4985
5081
  NavigatorRoles: () => NavigatorRoles,
4986
5082
  Navigators: () => Navigators,
5083
+ captureStrategyPressure: () => captureStrategyPressure,
4987
5084
  clearSrsBacklogDebug: () => clearSrsBacklogDebug,
5085
+ clearStrategyPressureDebug: () => clearStrategyPressureDebug,
4988
5086
  diversityRerank: () => diversityRerank,
4989
5087
  getActivePipeline: () => getActivePipeline,
4990
5088
  getCardOrigin: () => getCardOrigin,
@@ -4992,6 +5090,7 @@ __export(navigators_exports, {
4992
5090
  getRegisteredNavigatorNames: () => getRegisteredNavigatorNames,
4993
5091
  getRegisteredNavigatorRole: () => getRegisteredNavigatorRole,
4994
5092
  getSrsBacklogDebug: () => getSrsBacklogDebug,
5093
+ getStrategyPressureDebug: () => getStrategyPressureDebug,
4995
5094
  hasRegisteredNavigator: () => hasRegisteredNavigator,
4996
5095
  initializeNavigatorRegistry: () => initializeNavigatorRegistry,
4997
5096
  isFilter: () => isFilter,
@@ -5074,6 +5173,7 @@ var init_navigators = __esm({
5074
5173
  init_diversityRerank();
5075
5174
  init_PipelineDebugger();
5076
5175
  init_SrsDebugger();
5176
+ init_StrategyPressureDebugger();
5077
5177
  init_logger();
5078
5178
  init_();
5079
5179
  init_2();
@@ -6648,6 +6748,15 @@ var init_userOutcome = __esm({
6648
6748
  }
6649
6749
  });
6650
6750
 
6751
+ // src/core/types/hydration.ts
6752
+ var HYDRATION_MARKER_ID;
6753
+ var init_hydration = __esm({
6754
+ "src/core/types/hydration.ts"() {
6755
+ "use strict";
6756
+ HYDRATION_MARKER_ID = "_local/hydration";
6757
+ }
6758
+ });
6759
+
6651
6760
  // src/core/util/index.ts
6652
6761
  function getCardHistoryID(courseID, cardID) {
6653
6762
  return `${DocTypePrefixes["CARDRECORD" /* CARDRECORD */]}-${courseID}-${cardID}`;
@@ -7145,6 +7254,7 @@ var init_core = __esm({
7145
7254
  init_user();
7146
7255
  init_strategyState();
7147
7256
  init_userOutcome();
7257
+ init_hydration();
7148
7258
  init_Loggable();
7149
7259
  init_util();
7150
7260
  init_navigators();
@@ -7218,6 +7328,19 @@ var init_user_course_relDB = __esm({
7218
7328
  });
7219
7329
 
7220
7330
  // src/impl/common/BaseUserDB.ts
7331
+ function withTimeout(p, ms, label) {
7332
+ let timer;
7333
+ return Promise.race([
7334
+ p,
7335
+ new Promise((_resolve, reject) => {
7336
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
7337
+ })
7338
+ ]).finally(() => {
7339
+ if (timer !== void 0) {
7340
+ clearTimeout(timer);
7341
+ }
7342
+ });
7343
+ }
7221
7344
  function accomodateGuest() {
7222
7345
  logger.log("[funnel] accomodateGuest() called");
7223
7346
  if (typeof localStorage === "undefined") {
@@ -7391,7 +7514,7 @@ async function dropUserFromClassroom(user, classID) {
7391
7514
  async function getUserClassrooms(user) {
7392
7515
  return getOrCreateClassroomRegistrationsDoc(user);
7393
7516
  }
7394
- var import_common13, import_moment5, log3, BaseUser, userCoursesDoc, userClassroomsDoc;
7517
+ var import_common13, import_moment5, log3, HYDRATION_TIMEOUT_MS, BaseUser, userCoursesDoc, userClassroomsDoc;
7395
7518
  var init_BaseUserDB = __esm({
7396
7519
  "src/impl/common/BaseUserDB.ts"() {
7397
7520
  "use strict";
@@ -7408,6 +7531,7 @@ var init_BaseUserDB = __esm({
7408
7531
  log3 = (s) => {
7409
7532
  logger.info(s);
7410
7533
  };
7534
+ HYDRATION_TIMEOUT_MS = 15e3;
7411
7535
  BaseUser = class _BaseUser {
7412
7536
  static _instance;
7413
7537
  static _initialized = false;
@@ -7436,6 +7560,29 @@ var init_BaseUserDB = __esm({
7436
7560
  writeDB;
7437
7561
  // Database to use for write operations (local-first approach)
7438
7562
  updateQueue;
7563
+ _hydration = { state: "not-required" };
7564
+ /**
7565
+ * How far the local mirror can be trusted. See {@link UserHydrationStatus}.
7566
+ *
7567
+ * Consumers that would otherwise read a 404 as "new user" — onboarding
7568
+ * gates, first-run experiences, progress dashboards — should check for
7569
+ * `failed` and present a retry affordance rather than an empty state.
7570
+ */
7571
+ hydrationStatus() {
7572
+ return { ...this._hydration };
7573
+ }
7574
+ /**
7575
+ * Whether a missing document may be treated as genuinely absent, allowing
7576
+ * callers to create it with defaults.
7577
+ *
7578
+ * False only when hydration failed or is still running: a 404 then may just
7579
+ * mean "not pulled down yet", and materializing a default would both lie to
7580
+ * the user and, once live sync starts, push a rev-1 document that loses to
7581
+ * the real one — silently discarding it.
7582
+ */
7583
+ canMaterializeDefaults() {
7584
+ return this._hydration.state !== "failed" && this._hydration.state !== "hydrating";
7585
+ }
7439
7586
  async createAccount(username, password) {
7440
7587
  if (!this.syncStrategy.canCreateAccount()) {
7441
7588
  throw new Error("Account creation not supported by current sync strategy");
@@ -7553,6 +7700,11 @@ Currently logged-in as ${this._username}.`
7553
7700
  } catch (e) {
7554
7701
  const err = e;
7555
7702
  if (err.status === 404) {
7703
+ if (!this.canMaterializeDefaults()) {
7704
+ throw new Error(
7705
+ `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.`
7706
+ );
7707
+ }
7556
7708
  await this.localDB.put({
7557
7709
  _id: _BaseUser.DOC_IDS.COURSE_REGISTRATIONS,
7558
7710
  courses: [],
@@ -7795,6 +7947,11 @@ Currently logged-in as ${this._username}.`
7795
7947
  } catch (e) {
7796
7948
  const err = e;
7797
7949
  if (err.name && err.name === "not_found") {
7950
+ if (!this.canMaterializeDefaults()) {
7951
+ throw new Error(
7952
+ `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.`
7953
+ );
7954
+ }
7798
7955
  await this.localDB.put(defaultConfig);
7799
7956
  return this.getConfig();
7800
7957
  } else {
@@ -7868,7 +8025,9 @@ Currently logged-in as ${this._username}.`
7868
8025
  _BaseUser._initialized = true;
7869
8026
  return;
7870
8027
  }
8028
+ this.syncStrategy.stopSync?.();
7871
8029
  this.setDBandQ();
8030
+ await this.hydrateLocalMirror();
7872
8031
  this.syncStrategy.startSync(this.localDB, this.remoteDB);
7873
8032
  this.applyDesignDocs().catch((error) => {
7874
8033
  log3(`Error in applyDesignDocs background task: ${error}`);
@@ -7884,6 +8043,85 @@ Currently logged-in as ${this._username}.`
7884
8043
  });
7885
8044
  _BaseUser._initialized = true;
7886
8045
  }
8046
+ /**
8047
+ * Pull this account's documents into the local mirror, if that hasn't
8048
+ * happened on this device before.
8049
+ *
8050
+ * Runs between setDBandQ() and startSync() — after the handles exist, before
8051
+ * anything can observe an empty local DB or replicate one upward.
8052
+ *
8053
+ * Never throws: a failure is recorded as `failed` state and reported through
8054
+ * hydrationStatus(), because throwing here would abort init() and leave
8055
+ * BaseUser._initialized false forever (BaseUser.instance() polls it with no
8056
+ * ceiling). Callers decide what a failure means for them.
8057
+ */
8058
+ async hydrateLocalMirror() {
8059
+ if (this.localDB.name === this.remoteDB.name || !this.syncStrategy.hydrate) {
8060
+ this._hydration = { state: "not-required" };
8061
+ return;
8062
+ }
8063
+ if (await this.hasHydrationMarker()) {
8064
+ this._hydration = { state: "stale" };
8065
+ return;
8066
+ }
8067
+ this._hydration = { state: "hydrating" };
8068
+ const start = Date.now();
8069
+ try {
8070
+ const { docsWritten } = await withTimeout(
8071
+ this.syncStrategy.hydrate(this.localDB, this.remoteDB),
8072
+ HYDRATION_TIMEOUT_MS,
8073
+ `Hydration of local mirror for ${this._username}`
8074
+ );
8075
+ await this.writeHydrationMarker();
8076
+ this._hydration = {
8077
+ state: "hydrated",
8078
+ docsWritten,
8079
+ durationMs: Date.now() - start
8080
+ };
8081
+ log3(
8082
+ `Hydrated local mirror for ${this._username}: ${docsWritten} docs in ${this._hydration.durationMs}ms`
8083
+ );
8084
+ } catch (e) {
8085
+ this.syncStrategy.stopSync?.();
8086
+ this._hydration = {
8087
+ state: "failed",
8088
+ durationMs: Date.now() - start,
8089
+ error: e instanceof Error ? e.message : String(e)
8090
+ };
8091
+ logger.error(`Failed to hydrate local mirror for ${this._username}:`, e);
8092
+ }
8093
+ }
8094
+ /**
8095
+ * Has a full pull completed on this device for the CURRENT account?
8096
+ *
8097
+ * The marker is a `_local/` document, which never replicates — so its
8098
+ * presence describes this device's mirror and cannot arrive from elsewhere.
8099
+ */
8100
+ async hasHydrationMarker() {
8101
+ try {
8102
+ const marker = await this.localDB.get(HYDRATION_MARKER_ID);
8103
+ return marker.username === this._username;
8104
+ } catch {
8105
+ return false;
8106
+ }
8107
+ }
8108
+ async writeHydrationMarker() {
8109
+ try {
8110
+ let existingRev;
8111
+ try {
8112
+ existingRev = (await this.localDB.get(HYDRATION_MARKER_ID))._rev;
8113
+ } catch {
8114
+ }
8115
+ await this.localDB.put({
8116
+ _id: HYDRATION_MARKER_ID,
8117
+ _rev: existingRev,
8118
+ username: this._username,
8119
+ hydratedAt: (/* @__PURE__ */ new Date()).toISOString()
8120
+ });
8121
+ } catch (e) {
8122
+ logger.warn(`Could not write hydration marker for ${this._username}: ${e}`);
8123
+ }
8124
+ }
7887
8125
  static designDocs = [
7888
8126
  {
7889
8127
  _id: "_design/reviewCards",
@@ -8220,6 +8458,11 @@ Currently logged-in as ${this._username}.`
8220
8458
  } catch (e) {
8221
8459
  const err = e;
8222
8460
  if (err.status === 404) {
8461
+ if (!this.canMaterializeDefaults()) {
8462
+ throw new Error(
8463
+ `Cannot read strategy state '${strategyKey}' for ${this._username}: the local mirror is unavailable (hydration state '${this._hydration.state}').`
8464
+ );
8465
+ }
8223
8466
  return null;
8224
8467
  }
8225
8468
  throw e;
@@ -8744,6 +8987,8 @@ var init_CouchDBSyncStrategy = __esm({
8744
8987
  CouchDBSyncStrategy = class {
8745
8988
  syncHandle;
8746
8989
  // Handle to cancel sync if needed
8990
+ /** In-flight one-shot hydration pull, so a timed-out pull can be abandoned. */
8991
+ hydrationHandle;
8747
8992
  setupRemoteDB(username) {
8748
8993
  if (username === GuestUsername || username.startsWith(GuestUsername)) {
8749
8994
  return getLocalUserDB(username);
@@ -8758,8 +9003,26 @@ var init_CouchDBSyncStrategy = __esm({
8758
9003
  return this.getUserDB(username);
8759
9004
  }
8760
9005
  }
9006
+ /**
9007
+ * One-shot remote → local pull. Resolves once the local mirror is caught up.
9008
+ *
9009
+ * Pull only: pushing here would upload whatever the local DB happens to hold
9010
+ * before we know what the remote already has, which is the conflict-leaf
9011
+ * problem hydration exists to avoid. Local changes go up when startSync()
9012
+ * takes over.
9013
+ */
9014
+ async hydrate(localDB, remoteDB) {
9015
+ const replication = pouchdb_setup_default.replicate(remoteDB, localDB, {});
9016
+ this.hydrationHandle = replication;
9017
+ try {
9018
+ const info = await replication;
9019
+ return { docsWritten: info.docs_written };
9020
+ } finally {
9021
+ this.hydrationHandle = void 0;
9022
+ }
9023
+ }
8761
9024
  startSync(localDB, remoteDB) {
8762
- if (localDB !== remoteDB) {
9025
+ if (localDB.name !== remoteDB.name) {
8763
9026
  this.syncHandle = pouchdb_setup_default.sync(localDB, remoteDB, {
8764
9027
  live: true,
8765
9028
  retry: true
@@ -8767,8 +9030,20 @@ var init_CouchDBSyncStrategy = __esm({
8767
9030
  }
8768
9031
  }
8769
9032
  stopSync() {
9033
+ if (this.hydrationHandle) {
9034
+ try {
9035
+ this.hydrationHandle.cancel();
9036
+ } catch (e) {
9037
+ logger.warn(`Failed to cancel hydration pull (continuing anyway): ${e}`);
9038
+ }
9039
+ this.hydrationHandle = void 0;
9040
+ }
8770
9041
  if (this.syncHandle) {
8771
- this.syncHandle.cancel();
9042
+ try {
9043
+ this.syncHandle.cancel();
9044
+ } catch (e) {
9045
+ logger.warn(`Failed to cancel user sync (continuing anyway): ${e}`);
9046
+ }
8772
9047
  this.syncHandle = void 0;
8773
9048
  }
8774
9049
  }