@vue-skuilder/db 0.1.31-b → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/{contentSource-ygoFw9oV.d.ts → contentSource-Bdwkvqa8.d.ts} +16 -0
  2. package/dist/{contentSource-B7nXusjk.d.cts → contentSource-DF1nUbPQ.d.cts} +16 -0
  3. package/dist/core/index.d.cts +34 -3
  4. package/dist/core/index.d.ts +34 -3
  5. package/dist/core/index.js +510 -50
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +510 -50
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-BW7HvkMt.d.cts → dataLayerProvider-BKmVoyJR.d.ts} +20 -1
  10. package/dist/{dataLayerProvider-BfXUVDuG.d.ts → dataLayerProvider-BQdfJuBN.d.cts} +20 -1
  11. package/dist/impl/couch/index.d.cts +156 -4
  12. package/dist/impl/couch/index.d.ts +156 -4
  13. package/dist/impl/couch/index.js +730 -41
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +729 -41
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/index.d.cts +3 -2
  18. package/dist/impl/static/index.d.ts +3 -2
  19. package/dist/impl/static/index.js +467 -31
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +467 -31
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/index.d.cts +64 -3
  24. package/dist/index.d.ts +64 -3
  25. package/dist/index.js +948 -72
  26. package/dist/index.js.map +1 -1
  27. package/dist/index.mjs +948 -72
  28. package/dist/index.mjs.map +1 -1
  29. package/package.json +3 -3
  30. package/src/core/interfaces/contentSource.ts +6 -0
  31. package/src/core/interfaces/courseDB.ts +6 -0
  32. package/src/core/interfaces/dataLayerProvider.ts +20 -0
  33. package/src/core/navigators/Pipeline.ts +414 -9
  34. package/src/core/navigators/PipelineAssembler.ts +23 -18
  35. package/src/core/navigators/PipelineDebugger.ts +35 -1
  36. package/src/core/navigators/filters/hierarchyDefinition.ts +78 -8
  37. package/src/core/navigators/generators/prescribed.ts +95 -0
  38. package/src/core/navigators/index.ts +12 -0
  39. package/src/impl/common/BaseUserDB.ts +4 -1
  40. package/src/impl/couch/CourseSyncService.ts +356 -0
  41. package/src/impl/couch/PouchDataLayerProvider.ts +21 -1
  42. package/src/impl/couch/courseDB.ts +60 -13
  43. package/src/impl/couch/index.ts +1 -0
  44. package/src/impl/static/courseDB.ts +5 -0
  45. package/src/study/ItemQueue.ts +42 -0
  46. package/src/study/SessionController.ts +195 -22
  47. package/src/study/SpacedRepetition.ts +3 -1
  48. package/tests/core/navigators/Pipeline.test.ts +1 -1
  49. package/tests/core/navigators/PipelineAssembler.test.ts +15 -14
@@ -8,6 +8,21 @@ import {
8
8
  isFilter,
9
9
  } from './index';
10
10
  import { logger } from '../../util/logger';
11
+ import type { Pipeline, CardSpaceDiagnosis } from './Pipeline';
12
+
13
+ /**
14
+ * Captured reference to the most recently created Pipeline instance.
15
+ * Used by the debug API to run diagnostics against the live pipeline.
16
+ */
17
+ let _activePipeline: Pipeline | null = null;
18
+
19
+ /**
20
+ * Register a pipeline instance for diagnostic access.
21
+ * Called by Pipeline constructor.
22
+ */
23
+ export function registerPipelineForDebug(pipeline: Pipeline): void {
24
+ _activePipeline = pipeline;
25
+ }
11
26
 
12
27
  // ============================================================================
13
28
  // PIPELINE DEBUGGER
@@ -76,6 +91,7 @@ export interface PipelineRunReport {
76
91
  origin: 'new' | 'review' | 'unknown';
77
92
  finalScore: number;
78
93
  provenance: StrategyContribution[];
94
+ tags?: string[];
79
95
  selected: boolean;
80
96
  }>;
81
97
  }
@@ -135,6 +151,7 @@ export function buildRunReport(
135
151
  origin: getOrigin(card),
136
152
  finalScore: card.score,
137
153
  provenance: card.provenance,
154
+ tags: card.tags,
138
155
  selected: selectedIds.has(card.cardId),
139
156
  }));
140
157
 
@@ -459,6 +476,22 @@ export const pipelineDebugAPI = {
459
476
  console.groupEnd();
460
477
  },
461
478
 
479
+ /**
480
+ * Scan the full card space through the filter chain for the current user.
481
+ *
482
+ * Reports how many cards are well-indicated and how many are new.
483
+ * Use this to understand how the search space grows during onboarding.
484
+ *
485
+ * @param threshold - Score threshold for "well indicated" (default 0.10)
486
+ */
487
+ async diagnoseCardSpace(threshold?: number): Promise<CardSpaceDiagnosis | null> {
488
+ if (!_activePipeline) {
489
+ logger.info('[Pipeline Debug] No active pipeline. Run a session first.');
490
+ return null;
491
+ }
492
+ return _activePipeline.diagnoseCardSpace({ threshold });
493
+ },
494
+
462
495
  /**
463
496
  * Show help.
464
497
  */
@@ -471,6 +504,7 @@ Commands:
471
504
  .showRun(id|index) Show summary of a specific run (by index or ID suffix)
472
505
  .showCard(cardId) Show provenance trail for a specific card
473
506
  .explainReviews() Analyze why reviews were/weren't selected
507
+ .diagnoseCardSpace() Scan full card space through filters (async)
474
508
  .showRegistry() Show navigator registry (classes + roles)
475
509
  .showStrategies() Show registry + strategy mapping from last run
476
510
  .listRuns() List all captured runs in table format
@@ -482,7 +516,7 @@ Commands:
482
516
  Example:
483
517
  window.skuilder.pipeline.showLastRun()
484
518
  window.skuilder.pipeline.showRun(1)
485
- window.skuilder.pipeline.showCard('abc123')
519
+ await window.skuilder.pipeline.diagnoseCardSpace()
486
520
  `);
487
521
  },
488
522
  };
@@ -20,6 +20,15 @@ interface TagPrerequisite {
20
20
  /** Minimum interaction count (default: 3) */
21
21
  minCount?: number;
22
22
  };
23
+ /**
24
+ * Score multiplier applied to cards that carry this prereq tag while
25
+ * the gate is still closed. Steers the pipeline toward cards that help
26
+ * unlock the gated content. Falls away once the prereq is met.
27
+ *
28
+ * Example: `preReqBoost: 1.3` gives a 30% score increase to cards
29
+ * tagged `gpc:expose:t-T` while `gpc:intro:t-T` is still locked.
30
+ */
31
+ preReqBoost?: number;
23
32
  }
24
33
 
25
34
  /**
@@ -38,7 +47,7 @@ const DEFAULT_MIN_COUNT = 3;
38
47
  * A filter strategy that gates cards based on prerequisite mastery.
39
48
  *
40
49
  * Cards are locked until the user masters all prerequisite tags.
41
- * Locked cards receive score * 0.01 (strong penalty, not hard filter).
50
+ * Locked cards receive score * 0.05 (strong penalty, not hard filter).
42
51
  *
43
52
  * Mastery is determined by:
44
53
  * - User's ELO for the tag exceeds threshold (or avgElo if not specified)
@@ -94,8 +103,13 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
94
103
 
95
104
  if (prereq.masteryThreshold?.minElo !== undefined) {
96
105
  return userTagElo.score >= prereq.masteryThreshold.minElo;
106
+ } else if (prereq.masteryThreshold?.minCount !== undefined) {
107
+ // Explicit minCount without minElo: count alone is sufficient.
108
+ // The config author specified a concrete interaction threshold —
109
+ // don't additionally require above-average ELO.
110
+ return true;
97
111
  } else {
98
- // Default: user ELO for tag > global user ELO (proxy for "above average")
112
+ // No thresholds specified at all: fall back to above-average ELO
99
113
  return userTagElo.score >= userGlobalElo;
100
114
  }
101
115
  }
@@ -195,17 +209,51 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
195
209
  }
196
210
  }
197
211
 
212
+ /**
213
+ * Build a map of prereq tag → max configured boost for all *closed* gates.
214
+ *
215
+ * When a gate is closed (prereqs unmet), cards carrying that gate's prereq
216
+ * tags get boosted — steering the pipeline toward content that helps unlock
217
+ * the gated material. Once the gate opens, the boost disappears.
218
+ */
219
+ private getPreReqBoosts(
220
+ unlockedTags: Set<string>,
221
+ masteredTags: Set<string>
222
+ ): Map<string, number> {
223
+ const boosts = new Map<string, number>();
224
+
225
+ for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
226
+ // Only boost prereqs of closed gates
227
+ if (unlockedTags.has(tagId)) continue;
228
+
229
+ for (const prereq of prereqs) {
230
+ if (!prereq.preReqBoost || prereq.preReqBoost <= 1.0) continue;
231
+ // Only boost prereqs that aren't already met
232
+ if (masteredTags.has(prereq.tag)) continue;
233
+
234
+ const existing = boosts.get(prereq.tag) ?? 1.0;
235
+ boosts.set(prereq.tag, Math.max(existing, prereq.preReqBoost));
236
+ }
237
+ }
238
+
239
+ return boosts;
240
+ }
241
+
198
242
  /**
199
243
  * CardFilter.transform implementation.
200
244
  *
201
- * Apply prerequisite gating to cards. Cards with locked tags receive score * 0.01.
245
+ * Two effects:
246
+ * 1. Cards with locked tags receive score * 0.05 (gating penalty)
247
+ * 2. Cards carrying prereq tags of closed gates receive a configured
248
+ * boost (preReqBoost), steering toward content that unlocks gates
202
249
  */
203
250
  async transform(cards: WeightedCard[], context: FilterContext): Promise<WeightedCard[]> {
204
251
  // Get mastery state
205
252
  const masteredTags = await this.getMasteredTags(context);
206
253
  const unlockedTags = this.getUnlockedTags(masteredTags);
254
+ const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
207
255
 
208
- // Apply prerequisite gating as score multiplier
256
+ // Apply prerequisite gating + prereq boosting
209
257
  const gated: WeightedCard[] = [];
210
258
 
211
259
  for (const card of cards) {
@@ -215,9 +263,31 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
215
263
  unlockedTags,
216
264
  masteredTags
217
265
  );
218
- const LOCKED_PENALTY = 0.01;
219
- const finalScore = isUnlocked ? card.score : card.score * LOCKED_PENALTY;
220
- const action = isUnlocked ? 'passed' : 'penalized';
266
+ const LOCKED_PENALTY = 0.02;
267
+ let finalScore = isUnlocked ? card.score : card.score * LOCKED_PENALTY;
268
+ let action: 'passed' | 'penalized' | 'boosted' = isUnlocked ? 'passed' : 'penalized';
269
+ let finalReason = reason;
270
+
271
+ // Apply prereq boost to cards that passed gating (don't boost locked cards)
272
+ if (isUnlocked && preReqBoosts.size > 0) {
273
+ const cardTags = card.tags ?? [];
274
+ let maxBoost = 1.0;
275
+ const boostedPrereqs: string[] = [];
276
+
277
+ for (const tag of cardTags) {
278
+ const boost = preReqBoosts.get(tag);
279
+ if (boost && boost > maxBoost) {
280
+ maxBoost = boost;
281
+ boostedPrereqs.push(tag);
282
+ }
283
+ }
284
+
285
+ if (maxBoost > 1.0) {
286
+ finalScore *= maxBoost;
287
+ action = 'boosted';
288
+ finalReason = `${reason} | preReqBoost ×${maxBoost.toFixed(2)} for ${boostedPrereqs.join(', ')}`;
289
+ }
290
+ }
221
291
 
222
292
  gated.push({
223
293
  ...card,
@@ -230,7 +300,7 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
230
300
  strategyId: this.strategyId || 'NAVIGATION_STRATEGY-hierarchy',
231
301
  action,
232
302
  score: finalScore,
233
- reason,
303
+ reason: finalReason,
234
304
  },
235
305
  ],
236
306
  });
@@ -0,0 +1,95 @@
1
+ import type { CourseDBInterface } from '../../interfaces/courseDB';
2
+ import type { UserDBInterface } from '../../interfaces/userDB';
3
+ import { ContentNavigator } from '../index';
4
+ import type { WeightedCard } from '../index';
5
+ import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
6
+ import type { CardGenerator, GeneratorContext } from './types';
7
+ import { logger } from '@db/util/logger';
8
+
9
+ // ============================================================================
10
+ // PRESCRIBED CARDS GENERATOR
11
+ // ============================================================================
12
+ //
13
+ // A generator that always emits a configured list of card IDs at score 1.0.
14
+ //
15
+ // Use case: Cold-start curriculum bootstrapping. Ensures critical cards
16
+ // (e.g., intro-s, early WS exercises) are always in the candidate set
17
+ // regardless of ELO proximity sampling. Filters (hierarchy, priority)
18
+ // still run — cards whose utility has expired get penalized normally
19
+ // and drop out of the top-N selection.
20
+ //
21
+ // Config format:
22
+ // { "cardIds": ["c-intro-s-S", "c-ws-sit-abc123", ...] }
23
+ //
24
+ // ============================================================================
25
+
26
+ interface PrescribedConfig {
27
+ cardIds: string[];
28
+ }
29
+
30
+ export default class PrescribedCardsGenerator extends ContentNavigator implements CardGenerator {
31
+ name: string;
32
+ private config: PrescribedConfig;
33
+
34
+ constructor(
35
+ user: UserDBInterface,
36
+ course: CourseDBInterface,
37
+ strategyData: ContentNavigationStrategyData
38
+ ) {
39
+ super(user, course, strategyData);
40
+ this.name = strategyData.name || 'Prescribed Cards';
41
+
42
+ try {
43
+ const parsed = JSON.parse(strategyData.serializedData);
44
+ this.config = { cardIds: parsed.cardIds || [] };
45
+ } catch {
46
+ this.config = { cardIds: [] };
47
+ }
48
+
49
+ logger.debug(
50
+ `[Prescribed] Initialized with ${this.config.cardIds.length} prescribed cards`
51
+ );
52
+ }
53
+
54
+ async getWeightedCards(limit: number, _context?: GeneratorContext): Promise<WeightedCard[]> {
55
+ if (this.config.cardIds.length === 0) {
56
+ return [];
57
+ }
58
+
59
+ const courseId = this.course.getCourseID();
60
+
61
+ // Filter out cards the user has already interacted with
62
+ const activeCards = await this.user.getActiveCards();
63
+ const activeIds = new Set(activeCards.map((ac) => ac.cardID));
64
+ const eligibleIds = this.config.cardIds.filter((id) => !activeIds.has(id));
65
+
66
+ if (eligibleIds.length === 0) {
67
+ logger.debug('[Prescribed] All prescribed cards already active, returning empty');
68
+ return [];
69
+ }
70
+
71
+ // Emit at score 1.0 — CompositeGenerator deduplicates, and if ELO
72
+ // also surfaces the same card, the composite picks the higher score.
73
+ const cards: WeightedCard[] = eligibleIds.slice(0, limit).map((cardId) => ({
74
+ cardId,
75
+ courseId,
76
+ score: 1.0,
77
+ provenance: [
78
+ {
79
+ strategy: 'prescribed',
80
+ strategyName: this.strategyName || this.name,
81
+ strategyId: this.strategyId || 'NAVIGATION_STRATEGY-prescribed',
82
+ action: 'generated' as const,
83
+ score: 1.0,
84
+ reason: `Prescribed card (${eligibleIds.length} eligible of ${this.config.cardIds.length} configured)`,
85
+ },
86
+ ],
87
+ }));
88
+
89
+ logger.info(
90
+ `[Prescribed] Emitting ${cards.length} cards (${eligibleIds.length} eligible, ${activeIds.size} already active)`
91
+ );
92
+
93
+ return cards;
94
+ }
95
+ }
@@ -140,8 +140,10 @@ export async function initializeNavigatorRegistry(): Promise<void> {
140
140
  import('./generators/elo'),
141
141
  import('./generators/srs'),
142
142
  ]);
143
+ const prescribedModule = await import('./generators/prescribed');
143
144
  registerNavigator('elo', eloModule.default);
144
145
  registerNavigator('srs', srsModule.default);
146
+ registerNavigator('prescribed', prescribedModule.default);
145
147
 
146
148
  // Import and register filters
147
149
  const [
@@ -346,6 +348,7 @@ export function getCardOrigin(card: WeightedCard): 'new' | 'review' | 'failed' {
346
348
  export enum Navigators {
347
349
  ELO = 'elo',
348
350
  SRS = 'srs',
351
+ PRESCRIBED = 'prescribed',
349
352
  HIERARCHY = 'hierarchyDefinition',
350
353
  INTERFERENCE = 'interferenceMitigator',
351
354
  RELATIVE_PRIORITY = 'relativePriority',
@@ -384,6 +387,7 @@ export enum NavigatorRole {
384
387
  export const NavigatorRoles: Record<Navigators, NavigatorRole> = {
385
388
  [Navigators.ELO]: NavigatorRole.GENERATOR,
386
389
  [Navigators.SRS]: NavigatorRole.GENERATOR,
390
+ [Navigators.PRESCRIBED]: NavigatorRole.GENERATOR,
387
391
  [Navigators.HIERARCHY]: NavigatorRole.FILTER,
388
392
  [Navigators.INTERFERENCE]: NavigatorRole.FILTER,
389
393
  [Navigators.RELATIVE_PRIORITY]: NavigatorRole.FILTER,
@@ -624,4 +628,12 @@ export abstract class ContentNavigator implements StudyContentSource {
624
628
  async getWeightedCards(_limit: number): Promise<WeightedCard[]> {
625
629
  throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
626
630
  }
631
+
632
+ /**
633
+ * Set ephemeral hints for the next pipeline run.
634
+ * No-op for non-Pipeline navigators. Pipeline overrides this.
635
+ */
636
+ setEphemeralHints(_hints: Record<string, unknown>): void {
637
+ // no-op — only Pipeline implements this
638
+ }
627
639
  }
@@ -174,10 +174,13 @@ Currently logged-in as ${this._username}.`
174
174
  const docsToDelete = allDocs.rows
175
175
  .filter((row) => {
176
176
  const id = row.id;
177
- // Delete user progress data but preserve core user documents
177
+ // Delete user progress data but preserve authentication and user identity
178
178
  return (
179
179
  id.startsWith(DocTypePrefixes[DocType.CARDRECORD]) || // Card interaction history
180
180
  id.startsWith(DocTypePrefixes[DocType.SCHEDULED_CARD]) || // Scheduled reviews
181
+ id.startsWith(DocTypePrefixes[DocType.STRATEGY_STATE]) || // Strategy state (user prefs, progression)
182
+ id.startsWith(DocTypePrefixes[DocType.USER_OUTCOME]) || // Evolutionary orchestration outcomes
183
+ id.startsWith(DocTypePrefixes[DocType.STRATEGY_LEARNING_STATE]) || // Strategy learning state
181
184
  id === BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
182
185
  id === BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
183
186
  id === BaseUser.DOC_IDS.CONFIG // User config