@vue-skuilder/db 0.1.20 → 0.1.21

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 (70) hide show
  1. package/CLAUDE.md +2 -2
  2. package/dist/{classroomDB-CZdMBiTU.d.ts → contentSource-BP9hznNV.d.ts} +150 -196
  3. package/dist/{classroomDB-PxDZTky3.d.cts → contentSource-DsJadoBU.d.cts} +150 -196
  4. package/dist/core/index.d.cts +3 -3
  5. package/dist/core/index.d.ts +3 -3
  6. package/dist/core/index.js +615 -1758
  7. package/dist/core/index.js.map +1 -1
  8. package/dist/core/index.mjs +579 -1727
  9. package/dist/core/index.mjs.map +1 -1
  10. package/dist/{dataLayerProvider-D8o6ZnKW.d.ts → dataLayerProvider-CHYrQ5pB.d.cts} +1 -1
  11. package/dist/{dataLayerProvider-D0MoZMjH.d.cts → dataLayerProvider-MDTxXq2l.d.ts} +1 -1
  12. package/dist/impl/couch/index.d.cts +6 -22
  13. package/dist/impl/couch/index.d.ts +6 -22
  14. package/dist/impl/couch/index.js +598 -1769
  15. package/dist/impl/couch/index.js.map +1 -1
  16. package/dist/impl/couch/index.mjs +579 -1755
  17. package/dist/impl/couch/index.mjs.map +1 -1
  18. package/dist/impl/static/index.d.cts +22 -6
  19. package/dist/impl/static/index.d.ts +22 -6
  20. package/dist/impl/static/index.js +617 -1629
  21. package/dist/impl/static/index.js.map +1 -1
  22. package/dist/impl/static/index.mjs +607 -1624
  23. package/dist/impl/static/index.mjs.map +1 -1
  24. package/dist/index.d.cts +64 -56
  25. package/dist/index.d.ts +64 -56
  26. package/dist/index.js +1000 -2161
  27. package/dist/index.js.map +1 -1
  28. package/dist/index.mjs +970 -2127
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/pouch/index.js +3 -0
  31. package/dist/pouch/index.js.map +1 -1
  32. package/dist/pouch/index.mjs +3 -0
  33. package/dist/pouch/index.mjs.map +1 -1
  34. package/docs/navigators-architecture.md +2 -9
  35. package/package.json +3 -3
  36. package/src/core/interfaces/classroomDB.ts +5 -13
  37. package/src/core/interfaces/contentSource.ts +6 -66
  38. package/src/core/interfaces/courseDB.ts +2 -7
  39. package/src/core/navigators/Pipeline.ts +24 -53
  40. package/src/core/navigators/PipelineAssembler.ts +1 -1
  41. package/src/core/navigators/defaults.ts +84 -0
  42. package/src/core/navigators/{hierarchyDefinition.ts → filters/hierarchyDefinition.ts} +11 -25
  43. package/src/core/navigators/{interferenceMitigator.ts → filters/interferenceMitigator.ts} +10 -24
  44. package/src/core/navigators/{relativePriority.ts → filters/relativePriority.ts} +10 -24
  45. package/src/core/navigators/filters/userTagPreference.ts +1 -16
  46. package/src/core/navigators/{CompositeGenerator.ts → generators/CompositeGenerator.ts} +15 -64
  47. package/src/core/navigators/{elo.ts → generators/elo.ts} +13 -63
  48. package/src/core/navigators/{srs.ts → generators/srs.ts} +11 -40
  49. package/src/core/navigators/generators/types.ts +1 -1
  50. package/src/core/navigators/index.ts +36 -91
  51. package/src/impl/couch/classroomDB.ts +100 -103
  52. package/src/impl/couch/courseDB.ts +5 -81
  53. package/src/impl/couch/pouchdb-setup.ts +7 -0
  54. package/src/impl/static/StaticDataUnpacker.ts +50 -1
  55. package/src/impl/static/courseDB.ts +76 -37
  56. package/src/study/SessionController.ts +122 -202
  57. package/src/study/SourceMixer.ts +65 -0
  58. package/src/study/TagFilteredContentSource.ts +49 -92
  59. package/src/study/index.ts +1 -0
  60. package/src/study/services/CardHydrationService.ts +165 -81
  61. package/src/util/dataDirectory.ts +1 -1
  62. package/src/util/index.ts +0 -1
  63. package/tests/core/navigators/CompositeGenerator.test.ts +44 -168
  64. package/tests/core/navigators/Pipeline.test.ts +5 -72
  65. package/tests/core/navigators/PipelineAssembler.test.ts +8 -58
  66. package/tests/core/navigators/navigators.test.ts +118 -151
  67. package/src/core/navigators/hardcodedOrder.ts +0 -163
  68. package/src/util/tuiLogger.ts +0 -139
  69. /package/src/core/navigators/{inferredPreference.ts → filters/inferredPreferenceStub.ts} +0 -0
  70. /package/src/core/navigators/{userGoal.ts → filters/userGoalStub.ts} +0 -0
@@ -1,15 +1,11 @@
1
- import {
2
- StudyContentSource,
3
- StudySessionNewItem,
4
- StudySessionReviewItem,
5
- } from '@db/core/interfaces/contentSource';
1
+ import { StudyContentSource } from '@db/core/interfaces/contentSource';
6
2
  import { WeightedCard } from '@db/core/navigators';
7
3
  import { ClassroomConfig } from '@vue-skuilder/common';
8
4
  import { ENV } from '@db/factory';
9
5
  import { logger } from '@db/util/logger';
10
6
  import moment from 'moment';
11
7
  import pouch from './pouchdb-setup';
12
- import { getCourseDB, getStartAndEndKeys, createPouchDBConfig, REVIEW_TIME_FORMAT } from '.';
8
+ import { getStartAndEndKeys, createPouchDBConfig, REVIEW_TIME_FORMAT } from '.';
13
9
  import { CourseDB, getTag } from './courseDB';
14
10
 
15
11
  import { UserDBInterface } from '@db/core';
@@ -20,7 +16,6 @@ import {
20
16
  StudentClassroomDBInterface,
21
17
  TeacherClassroomDBInterface,
22
18
  } from '@db/core/interfaces/classroomDB';
23
- import { ScheduledCard } from '@db/core/types/user';
24
19
 
25
20
  const classroomLookupDBTitle = 'classdb-lookup';
26
21
  export const CLASSROOM_CONFIG = 'ClassroomConfig';
@@ -122,93 +117,30 @@ export class StudentClassroomDB
122
117
  void this.userMessages.on('change', f);
123
118
  }
124
119
 
125
- public async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
126
- const u = this._user;
127
- return (await u.getPendingReviews())
128
- .filter((r) => r.scheduledFor === 'classroom' && r.schedulingAgentId === this._id)
129
- .map((r) => {
130
- return {
131
- ...r,
132
- qualifiedID: `${r.courseId}-${r.cardId}`,
133
- courseID: r.courseId,
134
- cardID: r.cardId,
135
- contentSourceType: 'classroom',
136
- contentSourceID: this._id,
137
- reviewID: r._id,
138
- status: 'review',
139
- };
140
- });
141
- }
142
-
143
- public async getNewCards(): Promise<StudySessionNewItem[]> {
144
- const activeCards = await this._user.getActiveCards();
145
- const now = moment.utc();
146
- const assigned = await this.getAssignedContent();
147
- const due = assigned.filter((c) => now.isAfter(moment.utc(c.activeOn, REVIEW_TIME_FORMAT)));
148
-
149
- logger.info(`Due content: ${JSON.stringify(due)}`);
150
-
151
- let ret: StudySessionNewItem[] = [];
152
-
153
- for (let i = 0; i < due.length; i++) {
154
- const content = due[i];
155
-
156
- if (content.type === 'course') {
157
- const db = new CourseDB(content.courseID, async () => this._user);
158
- ret = ret.concat(await db.getNewCards());
159
- } else if (content.type === 'tag') {
160
- const tagDoc = await getTag(content.courseID, content.tagID);
161
-
162
- ret = ret.concat(
163
- tagDoc.taggedCards.map((c) => {
164
- return {
165
- courseID: content.courseID,
166
- cardID: c,
167
- qualifiedID: `${content.courseID}-${c}`,
168
- contentSourceType: 'classroom',
169
- contentSourceID: this._id,
170
- status: 'new',
171
- };
172
- })
173
- );
174
- } else if (content.type === 'card') {
175
- // returning card docs - not IDs
176
- ret.push(await getCourseDB(content.courseID).get(content.cardID));
177
- }
178
- }
179
-
180
- logger.info(
181
- `New Cards from classroom ${this._cfg.name}: ${ret.map((c) => `${c.courseID}-${c.cardID}`)}`
182
- );
183
-
184
- return ret.filter((c) => {
185
- if (activeCards.some((ac) => c.cardID === ac.cardID)) {
186
- // [ ] almost certainly broken after removing qualifiedID from StudySessionItem
187
- return false;
188
- } else {
189
- return true;
190
- }
191
- });
192
- }
193
-
194
120
  /**
195
121
  * Get cards with suitability scores for presentation.
196
122
  *
197
- * This implementation wraps the legacy getNewCards/getPendingReviews methods,
198
- * assigning score=1.0 to all cards. StudentClassroomDB does not currently
199
- * support pluggable navigation strategies.
123
+ * Gathers new cards from assigned content (courses, tags, cards) and
124
+ * pending reviews scheduled for this classroom. Assigns score=1.0 to all.
200
125
  *
201
126
  * @param limit - Maximum number of cards to return
202
127
  * @returns Cards sorted by score descending (all scores = 1.0)
203
128
  */
204
129
  public async getWeightedCards(limit: number): Promise<WeightedCard[]> {
205
- const [newCards, reviews] = await Promise.all([this.getNewCards(), this.getPendingReviews()]);
130
+ const weighted: WeightedCard[] = [];
131
+
132
+ // Get pending reviews for this classroom
133
+ const allUserReviews = await this._user.getPendingReviews();
134
+ const classroomReviews = allUserReviews.filter(
135
+ (r) => r.scheduledFor === 'classroom' && r.schedulingAgentId === this._id
136
+ );
206
137
 
207
- const weighted: WeightedCard[] = [
208
- ...newCards.map((c) => ({
209
- cardId: c.cardID,
210
- courseId: c.courseID,
138
+ for (const r of classroomReviews) {
139
+ weighted.push({
140
+ cardId: r.cardId,
141
+ courseId: r.courseId,
211
142
  score: 1.0,
143
+ reviewID: r._id,
212
144
  provenance: [
213
145
  {
214
146
  strategy: 'classroom',
@@ -216,29 +148,94 @@ export class StudentClassroomDB
216
148
  strategyId: 'CLASSROOM',
217
149
  action: 'generated' as const,
218
150
  score: 1.0,
219
- reason: 'Classroom legacy getNewCards(), new card',
151
+ reason: 'Classroom scheduled review',
220
152
  },
221
153
  ],
222
- })),
223
- ...reviews.map((r) => ({
224
- cardId: r.cardID,
225
- courseId: r.courseID,
226
- score: 1.0,
227
- provenance: [
228
- {
229
- strategy: 'classroom',
230
- strategyName: 'Classroom',
231
- strategyId: 'CLASSROOM',
232
- action: 'generated' as const,
154
+ });
155
+ }
156
+
157
+ // Get new cards from assigned content
158
+ const activeCards = await this._user.getActiveCards();
159
+ const activeCardIds = new Set(activeCards.map((ac) => ac.cardID));
160
+ const now = moment.utc();
161
+ const assigned = await this.getAssignedContent();
162
+ const due = assigned.filter((c) => now.isAfter(moment.utc(c.activeOn, REVIEW_TIME_FORMAT)));
163
+
164
+ logger.info(`[StudentClassroomDB] Due content: ${JSON.stringify(due)}`);
165
+
166
+ for (const content of due) {
167
+ if (content.type === 'course') {
168
+ // Get weighted cards from the course directly
169
+ const db = new CourseDB(content.courseID, async () => this._user);
170
+ const courseCards = await db.getWeightedCards(limit);
171
+ for (const card of courseCards) {
172
+ if (!activeCardIds.has(card.cardId)) {
173
+ weighted.push({
174
+ ...card,
175
+ provenance: [
176
+ ...card.provenance,
177
+ {
178
+ strategy: 'classroom',
179
+ strategyName: 'Classroom',
180
+ strategyId: 'CLASSROOM',
181
+ action: 'passed' as const,
182
+ score: card.score,
183
+ reason: `Assigned via classroom from course ${content.courseID}`,
184
+ },
185
+ ],
186
+ });
187
+ }
188
+ }
189
+ } else if (content.type === 'tag') {
190
+ const tagDoc = await getTag(content.courseID, content.tagID);
191
+
192
+ for (const cardId of tagDoc.taggedCards) {
193
+ if (!activeCardIds.has(cardId)) {
194
+ weighted.push({
195
+ cardId,
196
+ courseId: content.courseID,
197
+ score: 1.0,
198
+ provenance: [
199
+ {
200
+ strategy: 'classroom',
201
+ strategyName: 'Classroom',
202
+ strategyId: 'CLASSROOM',
203
+ action: 'generated' as const,
204
+ score: 1.0,
205
+ reason: `Classroom assigned tag: ${content.tagID}, new card`,
206
+ },
207
+ ],
208
+ });
209
+ }
210
+ }
211
+ } else if (content.type === 'card') {
212
+ if (!activeCardIds.has(content.cardID)) {
213
+ weighted.push({
214
+ cardId: content.cardID,
215
+ courseId: content.courseID,
233
216
  score: 1.0,
234
- reason: 'Classroom legacy getPendingReviews(), review',
235
- },
236
- ],
237
- })),
238
- ];
217
+ provenance: [
218
+ {
219
+ strategy: 'classroom',
220
+ strategyName: 'Classroom',
221
+ strategyId: 'CLASSROOM',
222
+ action: 'generated' as const,
223
+ score: 1.0,
224
+ reason: 'Classroom assigned card, new card',
225
+ },
226
+ ],
227
+ });
228
+ }
229
+ }
230
+ }
231
+
232
+ logger.info(
233
+ `[StudentClassroomDB] New cards from classroom ${this._cfg.name}: ` +
234
+ `${weighted.length} total (reviews + new)`
235
+ );
239
236
 
240
- // Sort by score descending (all 1.0 in this case) and limit
241
- return weighted.slice(0, limit);
237
+ // Sort by score descending and limit
238
+ return weighted.sort((a, b) => b.score - a.score).slice(0, limit);
242
239
  }
243
240
  }
244
241
 
@@ -1,5 +1,4 @@
1
1
  import { CourseDBInterface, CourseInfo, CoursesDBInterface, UserDBInterface } from '@db/core';
2
- import { ScheduledCard } from '@db/core/types/user';
3
2
  import {
4
3
  CourseConfig,
5
4
  CourseElo,
@@ -12,12 +11,7 @@ import {
12
11
 
13
12
  import { filterAllDocsByPrefix, getCourseDB, getCourseDoc, getCourseDocs } from '.';
14
13
  import UpdateQueue from './updateQueue';
15
- import {
16
- StudyContentSource,
17
- StudySessionItem,
18
- StudySessionNewItem,
19
- StudySessionReviewItem,
20
- } from '../../core/interfaces/contentSource';
14
+ import { StudySessionItem } from '../../core/interfaces/contentSource';
21
15
  import {
22
16
  CardData,
23
17
  DocType,
@@ -35,12 +29,8 @@ import { PouchError } from './types';
35
29
  import CourseLookup from './courseLookupDB';
36
30
  import { ContentNavigationStrategyData } from '@db/core/types/contentNavigationStrategy';
37
31
  import { ContentNavigator, Navigators, WeightedCard } from '@db/core/navigators';
38
- import { Pipeline } from '@db/core/navigators/Pipeline';
39
32
  import { PipelineAssembler } from '@db/core/navigators/PipelineAssembler';
40
- import CompositeGenerator from '@db/core/navigators/CompositeGenerator';
41
- import ELONavigator from '@db/core/navigators/elo';
42
- import SRSNavigator from '@db/core/navigators/srs';
43
- import { createEloDistanceFilter } from '@db/core/navigators/filters/eloDistance';
33
+ import { createDefaultPipeline } from '@db/core/navigators/defaults';
44
34
 
45
35
  export class CoursesDB implements CoursesDBInterface {
46
36
  _courseIDs: string[] | undefined;
@@ -99,7 +89,7 @@ function randIntWeightedTowardZero(n: number) {
99
89
  return Math.floor(Math.random() * Math.random() * Math.random() * n);
100
90
  }
101
91
 
102
- export class CourseDB implements StudyContentSource, CourseDBInterface {
92
+ export class CourseDB implements CourseDBInterface {
103
93
  // private log(msg: string): void {
104
94
  // log(`CourseLog: ${this.id}\n ${msg}`);
105
95
  // }
@@ -563,7 +553,7 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
563
553
  logger.debug(
564
554
  '[courseDB] No strategy documents found, using default Pipeline(Composite(ELO, SRS), [eloDistanceFilter])'
565
555
  );
566
- return this.createDefaultPipeline(user);
556
+ return createDefaultPipeline(user, this);
567
557
  }
568
558
 
569
559
  // Use PipelineAssembler to build a Pipeline from strategy documents
@@ -583,7 +573,7 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
583
573
  if (!pipeline) {
584
574
  // Assembly failed - fall back to default
585
575
  logger.debug('[courseDB] Pipeline assembly failed, using default pipeline');
586
- return this.createDefaultPipeline(user);
576
+ return createDefaultPipeline(user, this);
587
577
  }
588
578
 
589
579
  logger.debug(
@@ -596,48 +586,6 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
596
586
  }
597
587
  }
598
588
 
599
- private makeDefaultEloStrategy(): ContentNavigationStrategyData {
600
- return {
601
- _id: 'NAVIGATION_STRATEGY-ELO-default',
602
- docType: DocType.NAVIGATION_STRATEGY,
603
- name: 'ELO (default)',
604
- description: 'Default ELO-based navigation strategy for new cards',
605
- implementingClass: Navigators.ELO,
606
- course: this.id,
607
- serializedData: '',
608
- };
609
- }
610
-
611
- private makeDefaultSrsStrategy(): ContentNavigationStrategyData {
612
- return {
613
- _id: 'NAVIGATION_STRATEGY-SRS-default',
614
- docType: DocType.NAVIGATION_STRATEGY,
615
- name: 'SRS (default)',
616
- description: 'Default SRS-based navigation strategy for reviews',
617
- implementingClass: Navigators.SRS,
618
- course: this.id,
619
- serializedData: '',
620
- };
621
- }
622
-
623
- /**
624
- * Creates the default navigation pipeline for courses with no configured strategies.
625
- *
626
- * Default: Pipeline(Composite(ELO, SRS), [eloDistanceFilter])
627
- * - ELO generator: scores new cards by skill proximity
628
- * - SRS generator: scores reviews by overdueness and interval recency
629
- * - ELO distance filter: penalizes cards far from user's current level
630
- */
631
- private createDefaultPipeline(user: UserDBInterface): Pipeline {
632
- const eloNavigator = new ELONavigator(user, this, this.makeDefaultEloStrategy());
633
- const srsNavigator = new SRSNavigator(user, this, this.makeDefaultSrsStrategy());
634
-
635
- const compositeGenerator = new CompositeGenerator([eloNavigator, srsNavigator]);
636
- const eloDistanceFilter = createEloDistanceFilter();
637
-
638
- return new Pipeline(compositeGenerator, [eloDistanceFilter], user, this);
639
- }
640
-
641
589
  ////////////////////////////////////
642
590
  // END NavigationStrategyManager implementation
643
591
  ////////////////////////////////////
@@ -646,30 +594,6 @@ above:\n${above.rows.map((r) => `\t${r.id}-${r.key}\n`)}`;
646
594
  // StudyContentSource implementation
647
595
  ////////////////////////////////////
648
596
 
649
- public async getNewCards(limit: number = 99): Promise<StudySessionNewItem[]> {
650
- const u = await this._getCurrentUser();
651
-
652
- try {
653
- const navigator = await this.createNavigator(u);
654
- return navigator.getNewCards(limit);
655
- } catch (e) {
656
- logger.error(`[courseDB] Error in getNewCards: ${e}`);
657
- throw e;
658
- }
659
- }
660
-
661
- public async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
662
- const u = await this._getCurrentUser();
663
-
664
- try {
665
- const navigator = await this.createNavigator(u);
666
- return navigator.getPendingReviews();
667
- } catch (e) {
668
- logger.error(`[courseDB] Error in getPendingReviews: ${e}`);
669
- throw e;
670
- }
671
- }
672
-
673
597
  /**
674
598
  * Get cards with suitability scores for presentation.
675
599
  *
@@ -6,6 +6,13 @@ import PouchDBAuth from '@nilock2/pouchdb-authentication';
6
6
  PouchDB.plugin(PouchDBFind);
7
7
  PouchDB.plugin(PouchDBAuth);
8
8
 
9
+ // Disable PouchDB debug logging to prevent interference with CLI prompts
10
+ // Debug logging (like DerivedLogger.emit) will still go to the TUI log file
11
+ // if initializeTuiLogging() has been called, but won't clutter terminal output
12
+ if (typeof PouchDB.debug !== 'undefined') {
13
+ PouchDB.debug.disable();
14
+ }
15
+
9
16
  // Configure PouchDB globally
10
17
  PouchDB.defaults({
11
18
  // ajax: {
@@ -95,6 +95,47 @@ export class StaticDataUnpacker {
95
95
  throw new Error(`Document ${id} not found in chunk ${chunk.id}`);
96
96
  }
97
97
 
98
+ /**
99
+ * Get all documents with IDs starting with a specific prefix.
100
+ *
101
+ * This method loads the relevant chunk(s) and returns all matching documents.
102
+ * Useful for querying documents by type (e.g., all NAVIGATION_STRATEGY documents).
103
+ *
104
+ * @param prefix - Document ID prefix to match (e.g., "NAVIGATION_STRATEGY")
105
+ * @returns Array of all documents with IDs starting with the prefix
106
+ */
107
+ async getAllDocumentsByPrefix(prefix: string): Promise<any[]> {
108
+ // Find all chunks that could contain documents with this prefix
109
+ // A chunk contains documents if the prefix falls within its startKey/endKey range
110
+ const relevantChunks = this.manifest.chunks.filter((chunk) => {
111
+ // Check if prefix could be in this chunk's range
112
+ // Prefix matches if it's >= startKey and <= endKey (for lexicographic ordering)
113
+ const prefixEnd = prefix + '\ufff0'; // High unicode character for range end
114
+ return chunk.startKey <= prefixEnd && chunk.endKey >= prefix;
115
+ });
116
+
117
+ if (relevantChunks.length === 0) {
118
+ logger.debug(`[StaticDataUnpacker] No chunks found for prefix: ${prefix}`);
119
+ return [];
120
+ }
121
+
122
+ // Load all relevant chunks
123
+ await Promise.all(relevantChunks.map((chunk) => this.loadChunk(chunk.id)));
124
+
125
+ // Filter documents from cache that match the prefix
126
+ const matchingDocs: any[] = [];
127
+ for (const [docId, doc] of this.documentCache.entries()) {
128
+ if (docId.startsWith(prefix)) {
129
+ matchingDocs.push(await this.hydrateAttachments(doc));
130
+ }
131
+ }
132
+
133
+ logger.debug(
134
+ `[StaticDataUnpacker] Found ${matchingDocs.length} documents with prefix: ${prefix}`
135
+ );
136
+ return matchingDocs;
137
+ }
138
+
98
139
  /**
99
140
  * Query cards by ELO score, returning card IDs sorted by ELO
100
141
  */
@@ -148,7 +189,15 @@ export class StaticDataUnpacker {
148
189
  * Get all tag names mapped to their card arrays
149
190
  */
150
191
  async getTagsIndex(): Promise<TagsIndex> {
151
- return (await this.loadIndex('tags')) as TagsIndex;
192
+ try {
193
+ return (await this.loadIndex('tags')) as TagsIndex;
194
+ } catch {
195
+ // If no tags index exists, presume "no tagged cards"
196
+ return {
197
+ byCard: {},
198
+ byTag: {},
199
+ }
200
+ }
152
201
  }
153
202
 
154
203
  private getDocTypeFromId(id: string): DocType | undefined {
@@ -4,8 +4,7 @@ import {
4
4
  CourseDBInterface,
5
5
  UserDBInterface,
6
6
  CourseInfo,
7
- StudySessionNewItem,
8
- StudySessionReviewItem,
7
+ StudySessionItem,
9
8
  } from '../../core/interfaces';
10
9
  import { StaticDataUnpacker } from './StaticDataUnpacker';
11
10
  import { StaticCourseManifest } from '../../util/packer/types';
@@ -16,12 +15,15 @@ import {
16
15
  DocType,
17
16
  SkuilderCourseData,
18
17
  QualifiedCardID,
18
+ DocTypePrefixes,
19
19
  } from '../../core/types/types-legacy';
20
20
  import { DataLayerResult } from '../../core/types/db';
21
21
  import { ContentNavigationStrategyData } from '../../core/types/contentNavigationStrategy';
22
- import { ScheduledCard } from '../../core/types/user';
23
- import { Navigators } from '../../core/navigators';
22
+
23
+ import { ContentNavigator, WeightedCard } from '../../core/navigators';
24
24
  import { logger } from '../../util/logger';
25
+ import { createDefaultPipeline } from '@db/core/navigators/defaults';
26
+ import { PipelineAssembler } from '@db/core/navigators/PipelineAssembler';
25
27
 
26
28
  export class StaticCourseDB implements CourseDBInterface {
27
29
  constructor(
@@ -132,28 +134,10 @@ export class StaticCourseDB implements CourseDBInterface {
132
134
  return { ok: true, id: cardId, rev: '1-static' };
133
135
  }
134
136
 
135
- async getNewCards(limit: number = 99): Promise<StudySessionNewItem[]> {
136
- const activeCards = await this.userDB.getActiveCards();
137
- return (
138
- await this.getCardsCenteredAtELO({ limit: limit, elo: 'user' }, (c: QualifiedCardID) => {
139
- if (activeCards.some((ac) => c.cardID === ac.cardID)) {
140
- return false;
141
- } else {
142
- return true;
143
- }
144
- })
145
- ).map((c) => {
146
- return {
147
- ...c,
148
- status: 'new',
149
- };
150
- });
151
- }
152
-
153
137
  async getCardsCenteredAtELO(
154
138
  options: { limit: number; elo: 'user' | 'random' | number },
155
139
  filter?: (id: QualifiedCardID) => boolean
156
- ): Promise<StudySessionNewItem[]> {
140
+ ): Promise<StudySessionItem[]> {
157
141
  let targetElo = typeof options.elo === 'number' ? options.elo : 1000;
158
142
 
159
143
  if (options.elo === 'user') {
@@ -378,20 +362,24 @@ export class StaticCourseDB implements CourseDBInterface {
378
362
  }
379
363
 
380
364
  // Navigation Strategy Manager implementation
381
- async getNavigationStrategy(_id: string): Promise<ContentNavigationStrategyData> {
382
- return {
383
- _id: 'NAVIGATION_STRATEGY-ELO',
384
- docType: DocType.NAVIGATION_STRATEGY,
385
- name: 'ELO',
386
- description: 'ELO-based navigation strategy',
387
- implementingClass: Navigators.ELO,
388
- course: this.courseId,
389
- serializedData: '',
390
- };
365
+ async getNavigationStrategy(id: string): Promise<ContentNavigationStrategyData> {
366
+ try {
367
+ return await this.unpacker.getDocument(id);
368
+ } catch (error) {
369
+ logger.error(`[static/courseDB] Strategy ${id} not found: ${error}`);
370
+ throw error;
371
+ }
391
372
  }
392
373
 
393
374
  async getAllNavigationStrategies(): Promise<ContentNavigationStrategyData[]> {
394
- return [await this.getNavigationStrategy('ELO')];
375
+ const prefix = DocTypePrefixes[DocType.NAVIGATION_STRATEGY];
376
+ try {
377
+ const docs = await this.unpacker.getAllDocumentsByPrefix(prefix);
378
+ return docs as ContentNavigationStrategyData[];
379
+ } catch (error) {
380
+ logger.warn(`[static/courseDB] Error loading navigation strategies: ${error}`);
381
+ return []; // Fall back to default pipeline
382
+ }
395
383
  }
396
384
 
397
385
  async addNavigationStrategy(_data: ContentNavigationStrategyData): Promise<void> {
@@ -402,10 +390,61 @@ export class StaticCourseDB implements CourseDBInterface {
402
390
  throw new Error('Cannot update navigation strategies in static mode');
403
391
  }
404
392
 
393
+ /**
394
+ * Create a ContentNavigator for this course.
395
+ *
396
+ * Loads navigation strategy documents from static data and uses PipelineAssembler
397
+ * to build a Pipeline. Falls back to default pipeline if no strategies found.
398
+ */
399
+ async createNavigator(user: UserDBInterface): Promise<ContentNavigator> {
400
+ try {
401
+ const allStrategies = await this.getAllNavigationStrategies();
402
+
403
+ if (allStrategies.length === 0) {
404
+ logger.debug(
405
+ '[static/courseDB] No strategy documents found, using default Pipeline(Composite(ELO, SRS), [eloDistanceFilter])'
406
+ );
407
+ return createDefaultPipeline(user, this);
408
+ }
409
+
410
+ // Use PipelineAssembler to build Pipeline from strategy documents
411
+ const assembler = new PipelineAssembler();
412
+ const { pipeline, generatorStrategies, filterStrategies, warnings } =
413
+ await assembler.assemble({
414
+ strategies: allStrategies,
415
+ user,
416
+ course: this,
417
+ });
418
+
419
+ // Log warnings
420
+ for (const warning of warnings) {
421
+ logger.warn(`[PipelineAssembler] ${warning}`);
422
+ }
423
+
424
+ if (!pipeline) {
425
+ logger.debug('[static/courseDB] Pipeline assembly failed, using default pipeline');
426
+ return createDefaultPipeline(user, this);
427
+ }
428
+
429
+ logger.debug(
430
+ `[static/courseDB] Using assembled pipeline with ${generatorStrategies.length} generator(s) and ${filterStrategies.length} filter(s)`
431
+ );
432
+ return pipeline;
433
+ } catch (e) {
434
+ logger.error(`[static/courseDB] Error creating navigator: ${e}`);
435
+ throw e;
436
+ }
437
+ }
438
+
405
439
  // Study Content Source implementation
406
- async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
407
- // In static mode, reviews would be stored locally
408
- return [];
440
+ async getWeightedCards(limit: number): Promise<WeightedCard[]> {
441
+ try {
442
+ const navigator = await this.createNavigator(this.userDB);
443
+ return navigator.getWeightedCards(limit);
444
+ } catch (e) {
445
+ logger.error(`[static/courseDB] Error getting weighted cards: ${e}`);
446
+ throw e;
447
+ }
409
448
  }
410
449
 
411
450
  // Attachment helper methods (internal use, not part of interface)