@vue-skuilder/db 0.1.31 → 0.1.32-b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1913,6 +1913,7 @@ var init_hierarchyDefinition = __esm({
1913
1913
  "src/core/navigators/filters/hierarchyDefinition.ts"() {
1914
1914
  "use strict";
1915
1915
  init_navigators();
1916
+ init_logger();
1916
1917
  DEFAULT_MIN_COUNT = 3;
1917
1918
  HierarchyDefinitionNavigator = class extends ContentNavigator {
1918
1919
  config;
@@ -2080,6 +2081,9 @@ var init_hierarchyDefinition = __esm({
2080
2081
  finalScore *= maxBoost;
2081
2082
  action = "boosted";
2082
2083
  finalReason = `${reason} | preReqBoost \xD7${maxBoost.toFixed(2)} for ${boostedPrereqs.join(", ")}`;
2084
+ logger.info(
2085
+ `[HierarchyDefinition] preReqBoost \xD7${maxBoost.toFixed(2)} applied to card ${card.cardId} via tags [${boostedPrereqs.join(", ")}] (score: ${card.score.toFixed(3)} \u2192 ${finalScore.toFixed(3)})`
2086
+ );
2083
2087
  }
2084
2088
  }
2085
2089
  gated.push({
@@ -2589,7 +2593,7 @@ var init_relativePriority = __esm({
2589
2593
  const cardTags = card.tags ?? [];
2590
2594
  const priority = this.computeCardPriority(cardTags);
2591
2595
  const boostFactor = this.computeBoostFactor(priority);
2592
- const finalScore = Math.max(0, Math.min(1, card.score * boostFactor));
2596
+ const finalScore = Math.max(0, card.score * boostFactor);
2593
2597
  const action = boostFactor > 1 ? "boosted" : boostFactor < 1 ? "penalized" : "passed";
2594
2598
  const reason = this.buildPriorityReason(cardTags, priority, boostFactor, finalScore);
2595
2599
  return {
@@ -3354,7 +3358,7 @@ var init_Pipeline = __esm({
3354
3358
  card.provenance.push({
3355
3359
  strategy: "ephemeralHint",
3356
3360
  strategyId: "ephemeral-hint",
3357
- strategyName: "Replan Hint",
3361
+ strategyName: hints._label ? `Replan Hint (${hints._label})` : "Replan Hint",
3358
3362
  action: "boosted",
3359
3363
  score: card.score,
3360
3364
  reason: `boostTag ${pattern} \xD7${factor}`
@@ -3371,7 +3375,7 @@ var init_Pipeline = __esm({
3371
3375
  card.provenance.push({
3372
3376
  strategy: "ephemeralHint",
3373
3377
  strategyId: "ephemeral-hint",
3374
- strategyName: "Replan Hint",
3378
+ strategyName: hints._label ? `Replan Hint (${hints._label})` : "Replan Hint",
3375
3379
  action: "boosted",
3376
3380
  score: card.score,
3377
3381
  reason: `boostCard ${pattern} \xD7${factor}`
@@ -3381,6 +3385,7 @@ var init_Pipeline = __esm({
3381
3385
  }
3382
3386
  }
3383
3387
  const cardIds = new Set(cards.map((c) => c.cardId));
3388
+ const hintLabel = hints._label ? `Replan Hint (${hints._label})` : "Replan Hint";
3384
3389
  const inject = (card, reason) => {
3385
3390
  if (!cardIds.has(card.cardId)) {
3386
3391
  const floorScore = Math.max(card.score, 1);
@@ -3392,7 +3397,7 @@ var init_Pipeline = __esm({
3392
3397
  {
3393
3398
  strategy: "ephemeralHint",
3394
3399
  strategyId: "ephemeral-hint",
3395
- strategyName: "Replan Hint",
3400
+ strategyName: hintLabel,
3396
3401
  action: "boosted",
3397
3402
  score: floorScore,
3398
3403
  reason
@@ -4646,10 +4651,18 @@ ${above.rows.map((r) => ` ${r.id}-${r.key}
4646
4651
  * @param limit - Maximum number of cards to return
4647
4652
  * @returns Cards sorted by score descending
4648
4653
  */
4654
+ _pendingHints = null;
4655
+ setEphemeralHints(hints) {
4656
+ this._pendingHints = hints;
4657
+ }
4649
4658
  async getWeightedCards(limit) {
4650
4659
  const u = await this._getCurrentUser();
4651
4660
  try {
4652
4661
  const navigator = await this.createNavigator(u);
4662
+ if (this._pendingHints) {
4663
+ navigator.setEphemeralHints(this._pendingHints);
4664
+ this._pendingHints = null;
4665
+ }
4653
4666
  return navigator.getWeightedCards(limit);
4654
4667
  } catch (e) {
4655
4668
  logger.error(`[courseDB] Error getting weighted cards: ${e}`);
@@ -7640,9 +7653,17 @@ var init_courseDB2 = __esm({
7640
7653
  }
7641
7654
  }
7642
7655
  // Study Content Source implementation
7656
+ _pendingHints = null;
7657
+ setEphemeralHints(hints) {
7658
+ this._pendingHints = hints;
7659
+ }
7643
7660
  async getWeightedCards(limit) {
7644
7661
  try {
7645
7662
  const navigator = await this.createNavigator(this.userDB);
7663
+ if (this._pendingHints) {
7664
+ navigator.setEphemeralHints(this._pendingHints);
7665
+ this._pendingHints = null;
7666
+ }
7646
7667
  return navigator.getWeightedCards(limit);
7647
7668
  } catch (e) {
7648
7669
  logger.error(`[static/courseDB] Error getting weighted cards: ${e}`);
@@ -9319,8 +9340,9 @@ var ResponseProcessor = class {
9319
9340
  }
9320
9341
  try {
9321
9342
  const history = await cardHistory;
9343
+ let result;
9322
9344
  if (cardRecord.isCorrect) {
9323
- return this.processCorrectResponse(
9345
+ result = this.processCorrectResponse(
9324
9346
  cardRecord,
9325
9347
  history,
9326
9348
  studySessionItem,
@@ -9330,7 +9352,7 @@ var ResponseProcessor = class {
9330
9352
  cardId
9331
9353
  );
9332
9354
  } else {
9333
- return this.processIncorrectResponse(
9355
+ result = this.processIncorrectResponse(
9334
9356
  cardRecord,
9335
9357
  history,
9336
9358
  courseRegistrationDoc,
@@ -9342,6 +9364,18 @@ var ResponseProcessor = class {
9342
9364
  sessionViews
9343
9365
  );
9344
9366
  }
9367
+ if (cardRecord.deferAdvance && result.shouldLoadNextCard) {
9368
+ logger.info(
9369
+ "[ResponseProcessor] deferAdvance requested \u2014 suppressing navigation, action stashed:",
9370
+ { nextCardAction: result.nextCardAction }
9371
+ );
9372
+ result = {
9373
+ ...result,
9374
+ shouldLoadNextCard: false,
9375
+ deferred: true
9376
+ };
9377
+ }
9378
+ return result;
9345
9379
  } catch (e) {
9346
9380
  logger.error("[ResponseProcessor] Failed to load card history", { e, cardId });
9347
9381
  throw e;
@@ -11836,6 +11870,12 @@ var SessionController = class _SessionController extends Loggable {
11836
11870
  mixer;
11837
11871
  dataLayer;
11838
11872
  courseNameCache = /* @__PURE__ */ new Map();
11873
+ /**
11874
+ * Default pipeline batch size for new-card planning.
11875
+ * Set via constructor options; falls back to 20 when not specified.
11876
+ * Individual replans can override via `ReplanOptions.limit`.
11877
+ */
11878
+ _defaultBatchLimit = 20;
11839
11879
  sources;
11840
11880
  // dataLayer and getViewComponent now injected into CardHydrationService
11841
11881
  _sessionRecord = [];
@@ -11860,6 +11900,20 @@ var SessionController = class _SessionController extends Loggable {
11860
11900
  * (user state has changed from completing good cards).
11861
11901
  */
11862
11902
  _wellIndicatedRemaining = 0;
11903
+ /**
11904
+ * When true, suppresses the quality-based auto-replan trigger in
11905
+ * nextCard(). Set after a burst replan (small limit) to prevent the
11906
+ * auto-replan from clobbering the burst cards before they're consumed.
11907
+ * Cleared when the depletion-triggered replan fires (newQ exhausted).
11908
+ */
11909
+ _suppressQualityReplan = false;
11910
+ /**
11911
+ * Guards against infinite depletion-triggered replans. Set to true
11912
+ * when a depletion replan fires; cleared when a replan produces
11913
+ * content (newQ.length > 0 after replan) or when an explicit
11914
+ * (non-auto) replan is requested.
11915
+ */
11916
+ _depletionReplanAttempted = false;
11863
11917
  startTime;
11864
11918
  endTime;
11865
11919
  _secondsRemaining;
@@ -11884,8 +11938,12 @@ var SessionController = class _SessionController extends Loggable {
11884
11938
  * @param dataLayer - Data layer provider
11885
11939
  * @param getViewComponent - Function to resolve view components
11886
11940
  * @param mixer - Optional source mixer strategy (defaults to QuotaRoundRobinMixer)
11941
+ * @param options - Optional session-level configuration
11942
+ * @param options.defaultBatchLimit - Default pipeline batch size (default: 20).
11943
+ * Smaller values for newer users cause more frequent replans, keeping plans
11944
+ * aligned with rapidly-changing user state.
11887
11945
  */
11888
- constructor(sources, time, dataLayer, getViewComponent, mixer) {
11946
+ constructor(sources, time, dataLayer, getViewComponent, mixer, options) {
11889
11947
  super();
11890
11948
  this.dataLayer = dataLayer;
11891
11949
  this.mixer = mixer || new QuotaRoundRobinMixer();
@@ -11903,9 +11961,13 @@ var SessionController = class _SessionController extends Loggable {
11903
11961
  this.startTime = /* @__PURE__ */ new Date();
11904
11962
  this._secondsRemaining = time;
11905
11963
  this.endTime = new Date(this.startTime.valueOf() + 1e3 * this._secondsRemaining);
11964
+ if (options?.defaultBatchLimit !== void 0) {
11965
+ this._defaultBatchLimit = options.defaultBatchLimit;
11966
+ }
11906
11967
  this.log(`Session constructed:
11907
11968
  startTime: ${this.startTime}
11908
- endTime: ${this.endTime}`);
11969
+ endTime: ${this.endTime}
11970
+ defaultBatchLimit: ${this._defaultBatchLimit}`);
11909
11971
  }
11910
11972
  tick() {
11911
11973
  this._secondsRemaining = Math.floor((this.endTime.valueOf() - Date.now()) / 1e3);
@@ -11981,25 +12043,47 @@ var SessionController = class _SessionController extends Loggable {
11981
12043
  * Typical trigger: application-level code (e.g. after a GPC intro completion)
11982
12044
  * calls this to ensure newly-unlocked content appears in the session.
11983
12045
  */
11984
- async requestReplan(hints) {
12046
+ async requestReplan(options) {
12047
+ const opts = this.normalizeReplanOptions(options);
12048
+ if (opts.hints || opts.label || opts.limit) {
12049
+ this._depletionReplanAttempted = false;
12050
+ }
11985
12051
  if (this._replanPromise) {
11986
12052
  this.log("Replan already in progress, awaiting existing replan");
11987
12053
  return this._replanPromise;
11988
12054
  }
11989
- if (hints) {
12055
+ if (opts.hints) {
12056
+ const hintsWithLabel = opts.label ? { ...opts.hints, _label: opts.label } : opts.hints;
11990
12057
  for (const source of this.sources) {
11991
- this.log(`[Hints] source type=${source.constructor.name}, hasMethod=${typeof source.setEphemeralHints}`);
11992
- source.setEphemeralHints?.(hints);
12058
+ source.setEphemeralHints?.(hintsWithLabel);
11993
12059
  }
11994
12060
  }
11995
- this.log(`Mid-session replan requested${hints ? ` (hints: ${JSON.stringify(hints)})` : ""}`);
11996
- this._replanPromise = this._executeReplan();
12061
+ const labelTag = opts.label ? ` [${opts.label}]` : "";
12062
+ this.log(
12063
+ `Mid-session replan requested${labelTag} (limit: ${opts.limit ?? "default"}, mode: ${opts.mode ?? "replace"}${opts.hints ? ", with hints" : ""})`
12064
+ );
12065
+ this._replanPromise = this._executeReplan(opts);
11997
12066
  try {
11998
12067
  await this._replanPromise;
11999
12068
  } finally {
12000
12069
  this._replanPromise = null;
12001
12070
  }
12002
12071
  }
12072
+ /**
12073
+ * Normalise the requestReplan argument. Accepts either a ReplanOptions
12074
+ * object (new API) or a plain Record<string, unknown> (legacy callers
12075
+ * that passed hints directly). Distinguishes the two by checking for
12076
+ * the presence of ReplanOptions-specific keys.
12077
+ */
12078
+ normalizeReplanOptions(input) {
12079
+ if (!input) return {};
12080
+ const replanKeys = ["hints", "limit", "mode", "label"];
12081
+ const inputKeys = Object.keys(input);
12082
+ if (inputKeys.some((k) => replanKeys.includes(k))) {
12083
+ return input;
12084
+ }
12085
+ return { hints: input };
12086
+ }
12003
12087
  /** Minimum well-indicated cards before an additive retry is attempted */
12004
12088
  static MIN_WELL_INDICATED = 5;
12005
12089
  /**
@@ -12018,16 +12102,32 @@ var SessionController = class _SessionController extends Loggable {
12018
12102
  * pass all hierarchy filters, one additive retry is attempted — merging
12019
12103
  * any new high-quality candidates into the front of the queue.
12020
12104
  */
12021
- async _executeReplan() {
12022
- const wellIndicated = await this.getWeightedContent({ replan: true });
12105
+ async _executeReplan(opts = {}) {
12106
+ const limit = opts.limit;
12107
+ const mode = opts.mode ?? "replace";
12108
+ const wellIndicated = await this.getWeightedContent({
12109
+ replan: true,
12110
+ additive: mode === "merge",
12111
+ limit
12112
+ });
12023
12113
  this._wellIndicatedRemaining = wellIndicated;
12114
+ if (limit !== void 0 && limit < this._defaultBatchLimit) {
12115
+ this._suppressQualityReplan = true;
12116
+ this.log(`[Replan] Burst mode (limit=${limit}): suppressing quality-based auto-replan`);
12117
+ } else {
12118
+ this._suppressQualityReplan = false;
12119
+ }
12024
12120
  if (wellIndicated >= 0 && wellIndicated < _SessionController.MIN_WELL_INDICATED) {
12025
12121
  this.log(
12026
12122
  `[Replan] Only ${wellIndicated}/${_SessionController.MIN_WELL_INDICATED} well-indicated cards after replan`
12027
12123
  );
12028
12124
  }
12125
+ if (this.newQ.length > 0) {
12126
+ this._depletionReplanAttempted = false;
12127
+ }
12029
12128
  await this.hydrationService.ensureHydratedCards();
12030
- this.log(`Replan complete: newQ now has ${this.newQ.length} cards`);
12129
+ const labelTag = opts.label ? ` [${opts.label}]` : "";
12130
+ this.log(`Replan complete${labelTag}: newQ now has ${this.newQ.length} cards (mode=${mode})`);
12031
12131
  snapshotQueues(this.reviewQ.length, this.newQ.length, this.failedQ.length);
12032
12132
  }
12033
12133
  addTime(seconds) {
@@ -12087,7 +12187,9 @@ var SessionController = class _SessionController extends Loggable {
12087
12187
  cardIds: this.hydrationService.getHydratedCardIds()
12088
12188
  },
12089
12189
  replan: {
12090
- inProgress: this._replanPromise !== null
12190
+ inProgress: this._replanPromise !== null,
12191
+ suppressQualityReplan: this._suppressQualityReplan,
12192
+ defaultBatchLimit: this._defaultBatchLimit
12091
12193
  }
12092
12194
  };
12093
12195
  }
@@ -12114,7 +12216,7 @@ var SessionController = class _SessionController extends Loggable {
12114
12216
  async getWeightedContent(options) {
12115
12217
  const replan = options?.replan ?? false;
12116
12218
  const additive = options?.additive ?? false;
12117
- const limit = 20;
12219
+ const limit = options?.limit ?? this._defaultBatchLimit;
12118
12220
  const batches = [];
12119
12221
  for (let i = 0; i < this.sources.length; i++) {
12120
12222
  const source = this.sources[i];
@@ -12295,10 +12397,26 @@ var SessionController = class _SessionController extends Loggable {
12295
12397
  this.log("nextCard: awaiting in-flight replan before drawing");
12296
12398
  await this._replanPromise;
12297
12399
  }
12400
+ if (this.newQ.length <= 1 && this._secondsRemaining > 0 && !this._replanPromise && !this._depletionReplanAttempted) {
12401
+ this._suppressQualityReplan = false;
12402
+ this._depletionReplanAttempted = true;
12403
+ const otherContent = this.reviewQ.length + this.failedQ.length;
12404
+ if (this.newQ.length === 0 && otherContent === 0) {
12405
+ this.log(
12406
+ `[AutoReplan:depletion] All queues empty with ${this._secondsRemaining}s remaining. Awaiting replan.`
12407
+ );
12408
+ await this.requestReplan();
12409
+ } else {
12410
+ this.log(
12411
+ `[AutoReplan:depletion] newQ has ${this.newQ.length} card(s) (${otherContent} in other queues) with ${this._secondsRemaining}s remaining. Triggering background replan.`
12412
+ );
12413
+ void this.requestReplan();
12414
+ }
12415
+ }
12298
12416
  const REPLAN_BUFFER = 3;
12299
- if (this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
12417
+ if (!this._suppressQualityReplan && this._wellIndicatedRemaining <= REPLAN_BUFFER && this.newQ.length > 0 && !this._replanPromise) {
12300
12418
  this.log(
12301
- `[AutoReplan] ${this._wellIndicatedRemaining} well-indicated cards remaining (newQ: ${this.newQ.length}). Triggering background replan.`
12419
+ `[AutoReplan:quality] ${this._wellIndicatedRemaining} well-indicated cards remaining (newQ: ${this.newQ.length}). Triggering background replan.`
12302
12420
  );
12303
12421
  void this.requestReplan();
12304
12422
  }