@vue-skuilder/db 0.1.17 → 0.1.20

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 (92) hide show
  1. package/dist/{userDB-BqwxtJ_7.d.mts → classroomDB-CZdMBiTU.d.ts} +427 -104
  2. package/dist/{userDB-DNa0XPtn.d.ts → classroomDB-PxDZTky3.d.cts} +427 -104
  3. package/dist/core/index.d.cts +304 -0
  4. package/dist/core/index.d.ts +237 -25
  5. package/dist/core/index.js +2246 -118
  6. package/dist/core/index.js.map +1 -1
  7. package/dist/core/index.mjs +2235 -114
  8. package/dist/core/index.mjs.map +1 -1
  9. package/dist/{dataLayerProvider-VlngD19_.d.mts → dataLayerProvider-D0MoZMjH.d.cts} +1 -1
  10. package/dist/{dataLayerProvider-BV5iZqt_.d.ts → dataLayerProvider-D8o6ZnKW.d.ts} +1 -1
  11. package/dist/impl/couch/{index.d.mts → index.d.cts} +47 -5
  12. package/dist/impl/couch/index.d.ts +46 -4
  13. package/dist/impl/couch/index.js +2250 -134
  14. package/dist/impl/couch/index.js.map +1 -1
  15. package/dist/impl/couch/index.mjs +2212 -97
  16. package/dist/impl/couch/index.mjs.map +1 -1
  17. package/dist/impl/static/{index.d.mts → index.d.cts} +6 -6
  18. package/dist/impl/static/index.d.ts +5 -5
  19. package/dist/impl/static/index.js +1950 -143
  20. package/dist/impl/static/index.js.map +1 -1
  21. package/dist/impl/static/index.mjs +1922 -117
  22. package/dist/impl/static/index.mjs.map +1 -1
  23. package/dist/{index-Bmll7Xse.d.mts → index-B_j6u5E4.d.cts} +1 -1
  24. package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
  25. package/dist/{index.d.mts → index.d.cts} +97 -13
  26. package/dist/index.d.ts +96 -12
  27. package/dist/index.js +2439 -180
  28. package/dist/index.js.map +1 -1
  29. package/dist/index.mjs +2386 -135
  30. package/dist/index.mjs.map +1 -1
  31. package/dist/pouch/index.js +3 -3
  32. package/dist/{types-Dbp5DaRR.d.mts → types-Bn0itutr.d.cts} +1 -1
  33. package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
  34. package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.cts} +3 -1
  35. package/dist/{types-legacy-6ettoclI.d.mts → types-legacy-DDY4N-Uq.d.ts} +3 -1
  36. package/dist/util/packer/{index.d.mts → index.d.cts} +3 -3
  37. package/dist/util/packer/index.d.ts +3 -3
  38. package/dist/util/packer/index.js.map +1 -1
  39. package/dist/util/packer/index.mjs.map +1 -1
  40. package/docs/brainstorm-navigation-paradigm.md +369 -0
  41. package/docs/navigators-architecture.md +370 -0
  42. package/docs/todo-evolutionary-orchestration.md +310 -0
  43. package/docs/todo-nominal-tag-types.md +121 -0
  44. package/docs/todo-strategy-authoring.md +401 -0
  45. package/eslint.config.mjs +1 -1
  46. package/package.json +9 -4
  47. package/src/core/index.ts +1 -0
  48. package/src/core/interfaces/contentSource.ts +88 -4
  49. package/src/core/interfaces/courseDB.ts +13 -0
  50. package/src/core/interfaces/navigationStrategyManager.ts +0 -5
  51. package/src/core/interfaces/userDB.ts +32 -0
  52. package/src/core/navigators/CompositeGenerator.ts +268 -0
  53. package/src/core/navigators/Pipeline.ts +318 -0
  54. package/src/core/navigators/PipelineAssembler.ts +194 -0
  55. package/src/core/navigators/elo.ts +104 -15
  56. package/src/core/navigators/filters/eloDistance.ts +132 -0
  57. package/src/core/navigators/filters/index.ts +9 -0
  58. package/src/core/navigators/filters/types.ts +115 -0
  59. package/src/core/navigators/filters/userTagPreference.ts +232 -0
  60. package/src/core/navigators/generators/index.ts +2 -0
  61. package/src/core/navigators/generators/types.ts +107 -0
  62. package/src/core/navigators/hardcodedOrder.ts +111 -12
  63. package/src/core/navigators/hierarchyDefinition.ts +266 -0
  64. package/src/core/navigators/index.ts +404 -3
  65. package/src/core/navigators/inferredPreference.ts +107 -0
  66. package/src/core/navigators/interferenceMitigator.ts +355 -0
  67. package/src/core/navigators/relativePriority.ts +255 -0
  68. package/src/core/navigators/srs.ts +195 -0
  69. package/src/core/navigators/userGoal.ts +136 -0
  70. package/src/core/types/strategyState.ts +84 -0
  71. package/src/core/types/types-legacy.ts +2 -0
  72. package/src/impl/common/BaseUserDB.ts +74 -7
  73. package/src/impl/couch/adminDB.ts +1 -2
  74. package/src/impl/couch/classroomDB.ts +51 -0
  75. package/src/impl/couch/courseDB.ts +147 -49
  76. package/src/impl/static/courseDB.ts +11 -4
  77. package/src/study/SessionController.ts +149 -1
  78. package/src/study/TagFilteredContentSource.ts +255 -0
  79. package/src/study/index.ts +1 -0
  80. package/src/util/dataDirectory.test.ts +51 -22
  81. package/src/util/logger.ts +0 -1
  82. package/tests/core/navigators/CompositeGenerator.test.ts +455 -0
  83. package/tests/core/navigators/Pipeline.test.ts +406 -0
  84. package/tests/core/navigators/PipelineAssembler.test.ts +351 -0
  85. package/tests/core/navigators/SRSNavigator.test.ts +344 -0
  86. package/tests/core/navigators/eloDistanceFilter.test.ts +192 -0
  87. package/tests/core/navigators/navigators.test.ts +710 -0
  88. package/tsconfig.json +1 -1
  89. package/vitest.config.ts +29 -0
  90. package/dist/core/index.d.mts +0 -92
  91. /package/dist/{SyncStrategy-CyATpyLQ.d.mts → SyncStrategy-CyATpyLQ.d.cts} +0 -0
  92. /package/dist/pouch/{index.d.mts → index.d.cts} +0 -0
@@ -0,0 +1,232 @@
1
+ import type { ScheduledCard } from '../../types/user';
2
+ import type { CourseDBInterface } from '../../interfaces/courseDB';
3
+ import type { UserDBInterface } from '../../interfaces/userDB';
4
+ import { ContentNavigator } from '../index';
5
+ import type { WeightedCard } from '../index';
6
+ import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
7
+ import type { StudySessionReviewItem, StudySessionNewItem } from '../..';
8
+ import type { CardFilter, FilterContext } from './types';
9
+
10
+ // ============================================================================
11
+ // USER TAG PREFERENCE FILTER
12
+ // ============================================================================
13
+ //
14
+ // Allows users to personalize their learning experience by specifying:
15
+ // - Tags to boost/penalize (score multiplied by boost factor)
16
+ //
17
+ // User preferences are stored in STRATEGY_STATE documents in the user's
18
+ // database, enabling persistence across sessions and sync across devices.
19
+ //
20
+ // Use cases:
21
+ // - Goal-based learning: "I want to learn piano by ear, skip sight-reading"
22
+ // - Selective focus: "I only want to practice chess endgames"
23
+ // - Accessibility: "Skip text-heavy cards, prefer visual content"
24
+ // - Difficulty customization: "Skip beginner content I already know"
25
+ //
26
+ // ============================================================================
27
+
28
+ /**
29
+ * User's tag preference state, stored in STRATEGY_STATE document.
30
+ *
31
+ * This interface defines what gets persisted to the user's database.
32
+ * UI components write to this structure, and the filter reads from it.
33
+ *
34
+ * ## Preferences vs Goals
35
+ *
36
+ * Preferences are **path constraints** — they affect HOW the user learns,
37
+ * not WHAT they're trying to learn. Examples:
38
+ * - "Skip text-heavy cards" (accessibility)
39
+ * - "Prefer visual content"
40
+ *
41
+ * For **goal-based** filtering (defining WHAT to learn), see the separate
42
+ * UserGoalNavigator (stub). Goals affect progress tracking and completion
43
+ * criteria; preferences only affect card selection.
44
+ *
45
+ * ## Slider Semantics
46
+ *
47
+ * Each tag maps to a multiplier value in the `boost` record:
48
+ * - `0` = banish/exclude (card score = 0)
49
+ * - `0.5` = penalize by 50%
50
+ * - `1.0` = neutral/no effect (default when tag added)
51
+ * - `2.0` = 2x preference boost
52
+ * - Higher values = stronger preference
53
+ *
54
+ * If multiple tags on a card have preferences, the maximum multiplier wins.
55
+ */
56
+ export interface UserTagPreferenceState {
57
+ /**
58
+ * Tag-specific multipliers.
59
+ * Maps tag name to score multiplier (0 = exclude, 1 = neutral, >1 = boost).
60
+ */
61
+ boost: Record<string, number>;
62
+
63
+ /**
64
+ * ISO timestamp of last update.
65
+ * Use `moment.utc(updatedAt)` to parse into a Moment object.
66
+ */
67
+ updatedAt: string;
68
+ }
69
+
70
+ /**
71
+ * A filter that applies user-configured tag preferences.
72
+ *
73
+ * Reads preferences from STRATEGY_STATE document in user's database.
74
+ * If no preferences exist, passes through unchanged (no-op).
75
+ *
76
+ * Implements CardFilter for use in Pipeline architecture.
77
+ * Also extends ContentNavigator for compatibility with dynamic loading.
78
+ */
79
+ export default class UserTagPreferenceFilter extends ContentNavigator implements CardFilter {
80
+ private _strategyData: ContentNavigationStrategyData;
81
+
82
+ /** Human-readable name for CardFilter interface */
83
+ name: string;
84
+
85
+ constructor(
86
+ user: UserDBInterface,
87
+ course: CourseDBInterface,
88
+ strategyData: ContentNavigationStrategyData
89
+ ) {
90
+ super(user, course, strategyData);
91
+ this._strategyData = strategyData;
92
+ this.name = strategyData.name || 'User Tag Preferences';
93
+ }
94
+
95
+ /**
96
+ * Compute multiplier for a card based on its tags and user preferences.
97
+ * Returns the maximum multiplier among all matching tags, or 1.0 if no matches.
98
+ */
99
+ private computeMultiplier(cardTags: string[], boostMap: Record<string, number>): number {
100
+ const multipliers = cardTags
101
+ .map((tag) => boostMap[tag])
102
+ .filter((val) => val !== undefined);
103
+
104
+ if (multipliers.length === 0) {
105
+ return 1.0;
106
+ }
107
+
108
+ // Use max multiplier among matching tags
109
+ return Math.max(...multipliers);
110
+ }
111
+
112
+ /**
113
+ * Build human-readable reason for the filter's decision.
114
+ */
115
+ private buildReason(
116
+ cardTags: string[],
117
+ boostMap: Record<string, number>,
118
+ multiplier: number
119
+ ): string {
120
+ // Find which tag(s) contributed to the multiplier
121
+ const matchingTags = cardTags.filter((tag) => boostMap[tag] === multiplier);
122
+
123
+ if (multiplier === 0) {
124
+ return `Excluded by user preference: ${matchingTags.join(', ')} (${multiplier}x)`;
125
+ }
126
+
127
+ if (multiplier < 1.0) {
128
+ return `Penalized by user preference: ${matchingTags.join(', ')} (${multiplier.toFixed(2)}x)`;
129
+ }
130
+
131
+ if (multiplier > 1.0) {
132
+ return `Boosted by user preference: ${matchingTags.join(', ')} (${multiplier.toFixed(2)}x)`;
133
+ }
134
+
135
+ return 'No matching user preferences';
136
+ }
137
+
138
+ /**
139
+ * CardFilter.transform implementation.
140
+ *
141
+ * Apply user tag preferences:
142
+ * 1. Read preferences from strategy state
143
+ * 2. If no preferences, pass through unchanged
144
+ * 3. For each card:
145
+ * - Look up tag in boost record
146
+ * - If tag found: apply multiplier (0 = exclude, 1 = neutral, >1 = boost)
147
+ * - If multiple tags match: use max multiplier
148
+ * - Append provenance with clear reason
149
+ */
150
+ async transform(cards: WeightedCard[], _context: FilterContext): Promise<WeightedCard[]> {
151
+ // Read user preferences from strategy state
152
+ const prefs = await this.getStrategyState<UserTagPreferenceState>();
153
+
154
+
155
+ // No preferences configured → pass through unchanged
156
+ if (!prefs || Object.keys(prefs.boost).length === 0) {
157
+ return cards.map((card) => ({
158
+ ...card,
159
+ provenance: [
160
+ ...card.provenance,
161
+ {
162
+ strategy: 'userTagPreference',
163
+ strategyName: this.strategyName || this.name,
164
+ strategyId: this.strategyId || this._strategyData._id,
165
+ action: 'passed' as const,
166
+ score: card.score,
167
+ reason: 'No user tag preferences configured',
168
+ },
169
+ ],
170
+ }));
171
+ }
172
+
173
+ // Process each card
174
+ const adjusted: WeightedCard[] = await Promise.all(
175
+ cards.map(async (card) => {
176
+ const cardTags = card.tags ?? [];
177
+
178
+ // Compute multiplier based on card tags and user preferences
179
+ const multiplier = this.computeMultiplier(cardTags, prefs.boost);
180
+ const finalScore = Math.min(1, card.score * multiplier);
181
+
182
+ // Determine action for provenance
183
+ let action: 'passed' | 'boosted' | 'penalized';
184
+ if (multiplier === 0 || multiplier < 1.0) {
185
+ action = 'penalized';
186
+ } else if (multiplier > 1.0) {
187
+ action = 'boosted';
188
+ } else {
189
+ action = 'passed';
190
+ }
191
+
192
+ return {
193
+ ...card,
194
+ score: finalScore,
195
+ provenance: [
196
+ ...card.provenance,
197
+ {
198
+ strategy: 'userTagPreference',
199
+ strategyName: this.strategyName || this.name,
200
+ strategyId: this.strategyId || this._strategyData._id,
201
+ action,
202
+ score: finalScore,
203
+ reason: this.buildReason(cardTags, prefs.boost, multiplier),
204
+ },
205
+ ],
206
+ };
207
+ })
208
+ );
209
+
210
+ return adjusted;
211
+ }
212
+
213
+ /**
214
+ * Legacy getWeightedCards - throws as filters should not be used as generators.
215
+ */
216
+ async getWeightedCards(_limit: number): Promise<WeightedCard[]> {
217
+ throw new Error(
218
+ 'UserTagPreferenceFilter is a filter and should not be used as a generator. ' +
219
+ 'Use Pipeline with a generator and this filter via transform().'
220
+ );
221
+ }
222
+
223
+ // Legacy methods - stub implementations since filters don't generate cards
224
+
225
+ async getNewCards(_n?: number): Promise<StudySessionNewItem[]> {
226
+ return [];
227
+ }
228
+
229
+ async getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]> {
230
+ return [];
231
+ }
232
+ }
@@ -0,0 +1,2 @@
1
+ // Generator types and interfaces
2
+ export type { CardGenerator, GeneratorContext, CardGeneratorFactory } from './types';
@@ -0,0 +1,107 @@
1
+ import type { WeightedCard } from '../index';
2
+ import type { CourseDBInterface } from '../../interfaces/courseDB';
3
+ import type { UserDBInterface } from '../../interfaces/userDB';
4
+
5
+ // ============================================================================
6
+ // CARD GENERATOR INTERFACE
7
+ // ============================================================================
8
+ //
9
+ // Generators produce candidate cards with initial scores.
10
+ // They are the "source" stage of a navigation pipeline.
11
+ //
12
+ // Examples: ELO (skill proximity), SRS (review scheduling), HardcodedOrder
13
+ //
14
+ // Generators differ from filters:
15
+ // - Generators: produce candidates from DB queries, assign initial scores
16
+ // - Filters: transform existing candidates, adjust scores with multipliers
17
+ //
18
+ // The Pipeline class orchestrates: Generator → Filter₁ → Filter₂ → ... → Results
19
+ //
20
+ // ============================================================================
21
+
22
+ /**
23
+ * Context available to generators when producing candidates.
24
+ *
25
+ * Built once per getWeightedCards() call by the Pipeline.
26
+ */
27
+ export interface GeneratorContext {
28
+ /** User database interface */
29
+ user: UserDBInterface;
30
+
31
+ /** Course database interface */
32
+ course: CourseDBInterface;
33
+
34
+ /** User's global ELO score for this course */
35
+ userElo: number;
36
+
37
+ // Future extensions:
38
+ // - user's tag-level ELO data
39
+ // - course config
40
+ // - session state (cards already seen this session)
41
+ }
42
+
43
+ /**
44
+ * A generator that produces candidate cards with initial scores.
45
+ *
46
+ * Generators are the "source" stage of a navigation pipeline.
47
+ * They query the database for eligible cards and assign initial
48
+ * suitability scores based on their strategy (ELO proximity,
49
+ * review urgency, fixed order, etc.).
50
+ *
51
+ * ## Implementation Guidelines
52
+ *
53
+ * 1. **Create provenance**: Each card should have a provenance entry
54
+ * with action='generated' documenting why it was selected.
55
+ *
56
+ * 2. **Score semantics**: Higher scores = more suitable for presentation.
57
+ * Scores should be in [0, 1] range for composability.
58
+ *
59
+ * 3. **Limit handling**: Respect the limit parameter, but may over-fetch
60
+ * internally if needed for scoring accuracy.
61
+ *
62
+ * 4. **Sort before returning**: Return cards sorted by score descending.
63
+ *
64
+ * ## Example Implementation
65
+ *
66
+ * ```typescript
67
+ * const myGenerator: CardGenerator = {
68
+ * name: 'My Generator',
69
+ * async getWeightedCards(limit, context) {
70
+ * const candidates = await fetchCandidates(context.course, limit);
71
+ * return candidates.map(c => ({
72
+ * cardId: c.id,
73
+ * courseId: context.course.getCourseID(),
74
+ * score: computeScore(c, context),
75
+ * provenance: [{
76
+ * strategy: 'myGenerator',
77
+ * strategyName: 'My Generator',
78
+ * strategyId: 'MY_GENERATOR',
79
+ * action: 'generated',
80
+ * score: computeScore(c, context),
81
+ * reason: 'Explanation of selection'
82
+ * }]
83
+ * }));
84
+ * }
85
+ * };
86
+ * ```
87
+ */
88
+ export interface CardGenerator {
89
+ /** Human-readable name for this generator */
90
+ name: string;
91
+
92
+ /**
93
+ * Produce candidate cards with initial scores.
94
+ *
95
+ * @param limit - Maximum number of cards to return
96
+ * @param context - Shared context (user, course, userElo, etc.)
97
+ * @returns Cards sorted by score descending, with provenance
98
+ */
99
+ getWeightedCards(limit: number, context: GeneratorContext): Promise<WeightedCard[]>;
100
+ }
101
+
102
+ /**
103
+ * Factory function type for creating generators from configuration.
104
+ *
105
+ * Used by PipelineAssembler to instantiate generators from strategy documents.
106
+ */
107
+ export type CardGeneratorFactory<TConfig = unknown> = (config: TConfig) => CardGenerator;
@@ -1,22 +1,55 @@
1
- import { CourseDBInterface, QualifiedCardID, StudySessionNewItem, StudySessionReviewItem, UserDBInterface } from '..';
2
- import { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
3
- import { ScheduledCard } from '../types/user';
1
+ import type {
2
+ CourseDBInterface,
3
+ QualifiedCardID,
4
+ StudySessionNewItem,
5
+ StudySessionReviewItem,
6
+ UserDBInterface,
7
+ } from '..';
8
+ import type { ContentNavigationStrategyData } from '../types/contentNavigationStrategy';
9
+ import type { ScheduledCard } from '../types/user';
4
10
  import { ContentNavigator } from './index';
11
+ import type { WeightedCard } from './index';
12
+ import type { CardGenerator, GeneratorContext } from './generators/types';
5
13
  import { logger } from '../../util/logger';
6
14
 
7
- export default class HardcodedOrderNavigator extends ContentNavigator {
15
+ // ============================================================================
16
+ // HARDCODED ORDER NAVIGATOR
17
+ // ============================================================================
18
+ //
19
+ // A generator strategy that presents cards in a fixed, author-defined order.
20
+ //
21
+ // Use case: When course authors want explicit control over content sequencing,
22
+ // e.g., teaching letters in a specific pedagogical order.
23
+ //
24
+ // The order is defined in serializedData as a JSON array of card IDs.
25
+ // Earlier positions in the array get higher scores.
26
+ //
27
+ // ============================================================================
28
+
29
+ /**
30
+ * A navigation strategy that presents cards in a fixed order.
31
+ *
32
+ * Implements CardGenerator for use in Pipeline architecture.
33
+ * Also extends ContentNavigator for backward compatibility with legacy code.
34
+ *
35
+ * Scoring:
36
+ * - Earlier cards in the sequence get higher scores
37
+ * - Reviews get score 1.0 (highest priority)
38
+ * - New cards scored by position: 1.0 - (position / total) * 0.5
39
+ */
40
+ export default class HardcodedOrderNavigator extends ContentNavigator implements CardGenerator {
41
+ /** Human-readable name for CardGenerator interface */
42
+ name: string;
43
+
8
44
  private orderedCardIds: string[] = [];
9
- private user: UserDBInterface;
10
- private course: CourseDBInterface;
11
45
 
12
46
  constructor(
13
47
  user: UserDBInterface,
14
48
  course: CourseDBInterface,
15
49
  strategyData: ContentNavigationStrategyData
16
50
  ) {
17
- super();
18
- this.user = user;
19
- this.course = course;
51
+ super(user, course, strategyData);
52
+ this.name = strategyData.name || 'Hardcoded Order';
20
53
 
21
54
  if (strategyData.serializedData) {
22
55
  try {
@@ -45,9 +78,7 @@ export default class HardcodedOrderNavigator extends ContentNavigator {
45
78
  async getNewCards(limit: number = 99): Promise<StudySessionNewItem[]> {
46
79
  const activeCardIds = (await this.user.getActiveCards()).map((c: QualifiedCardID) => c.cardID);
47
80
 
48
- const newCardIds = this.orderedCardIds.filter(
49
- (cardId) => !activeCardIds.includes(cardId)
50
- );
81
+ const newCardIds = this.orderedCardIds.filter((cardId) => !activeCardIds.includes(cardId));
51
82
 
52
83
  const cardsToReturn = newCardIds.slice(0, limit);
53
84
 
@@ -61,4 +92,72 @@ export default class HardcodedOrderNavigator extends ContentNavigator {
61
92
  };
62
93
  });
63
94
  }
95
+
96
+ /**
97
+ * Get cards in hardcoded order with scores based on position.
98
+ *
99
+ * Earlier cards in the sequence get higher scores.
100
+ * Score formula: 1.0 - (position / totalCards) * 0.5
101
+ * This ensures scores range from 1.0 (first card) to 0.5+ (last card).
102
+ *
103
+ * This method supports both the legacy signature (limit only) and the
104
+ * CardGenerator interface signature (limit, context).
105
+ *
106
+ * @param limit - Maximum number of cards to return
107
+ * @param _context - Optional GeneratorContext (currently unused, but required for interface)
108
+ */
109
+ async getWeightedCards(limit: number, _context?: GeneratorContext): Promise<WeightedCard[]> {
110
+ const activeCardIds = (await this.user.getActiveCards()).map((c: QualifiedCardID) => c.cardID);
111
+ const reviews = await this.getPendingReviews();
112
+
113
+ // Filter out already-active cards
114
+ const newCardIds = this.orderedCardIds.filter((cardId) => !activeCardIds.includes(cardId));
115
+
116
+ const totalCards = newCardIds.length;
117
+
118
+ // Score new cards by position in sequence
119
+ const scoredNew: WeightedCard[] = newCardIds.slice(0, limit).map((cardId, index) => {
120
+ const position = index + 1;
121
+ const score = Math.max(0.5, 1.0 - (index / totalCards) * 0.5);
122
+
123
+ return {
124
+ cardId,
125
+ courseId: this.course.getCourseID(),
126
+ score,
127
+ provenance: [
128
+ {
129
+ strategy: 'hardcodedOrder',
130
+ strategyName: this.strategyName || this.name,
131
+ strategyId: this.strategyId || 'NAVIGATION_STRATEGY-hardcoded',
132
+ action: 'generated',
133
+ score,
134
+ reason: `Position ${position} of ${totalCards} in fixed sequence, new card`,
135
+ },
136
+ ],
137
+ };
138
+ });
139
+
140
+ // Score reviews at 1.0 (highest priority)
141
+ const scoredReviews: WeightedCard[] = reviews.map((r) => ({
142
+ cardId: r.cardID,
143
+ courseId: r.courseID,
144
+ score: 1.0,
145
+ provenance: [
146
+ {
147
+ strategy: 'hardcodedOrder',
148
+ strategyName: this.strategyName || this.name,
149
+ strategyId: this.strategyId || 'NAVIGATION_STRATEGY-hardcoded',
150
+ action: 'generated',
151
+ score: 1.0,
152
+ reason: 'Scheduled review, highest priority',
153
+ },
154
+ ],
155
+ }));
156
+
157
+ // Combine (reviews already sorted at top due to score=1.0)
158
+ const all = [...scoredReviews, ...scoredNew];
159
+ all.sort((a, b) => b.score - a.score);
160
+
161
+ return all.slice(0, limit);
162
+ }
64
163
  }