@contractspec/example.learning-patterns 3.7.5 → 3.7.7

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.
@@ -1,256 +1,254 @@
1
1
  import { describe, expect, it } from 'bun:test';
2
-
3
- import type {
4
- LearningJourneyTrackSpec,
5
- StepAvailabilitySpec,
6
- StepCompletionConditionSpec,
7
- } from '@contractspec/module.learning-journey/track-spec';
8
2
  import { SRSEngine } from '@contractspec/module.learning-journey/engines/srs';
9
3
  import { StreakEngine } from '@contractspec/module.learning-journey/engines/streak';
10
4
  import { XPEngine } from '@contractspec/module.learning-journey/engines/xp';
11
-
5
+ import type {
6
+ LearningJourneyTrackSpec,
7
+ StepAvailabilitySpec,
8
+ StepCompletionConditionSpec,
9
+ } from '@contractspec/module.learning-journey/track-spec';
10
+ import { LEARNING_EVENTS } from './events';
12
11
  import { ambientCoachTrack } from './tracks/ambient-coach';
13
12
  import { drillsTrack } from './tracks/drills';
14
13
  import { questTrack } from './tracks/quests';
15
- import { LEARNING_EVENTS } from './events';
16
14
 
17
15
  interface LearningEvent {
18
- name: string;
19
- payload?: Record<string, unknown>;
20
- occurredAt?: Date;
16
+ name: string;
17
+ payload?: Record<string, unknown>;
18
+ occurredAt?: Date;
21
19
  }
22
20
 
23
21
  interface StepState {
24
- id: string;
25
- status: 'PENDING' | 'COMPLETED';
26
- occurrences: number;
27
- masteryCount: number;
28
- availableAt?: Date;
29
- dueAt?: Date;
22
+ id: string;
23
+ status: 'PENDING' | 'COMPLETED';
24
+ occurrences: number;
25
+ masteryCount: number;
26
+ availableAt?: Date;
27
+ dueAt?: Date;
30
28
  }
31
29
 
32
30
  const matchesFilter = (
33
- filter: Record<string, unknown> | undefined,
34
- payload: Record<string, unknown> | undefined
31
+ filter: Record<string, unknown> | undefined,
32
+ payload: Record<string, unknown> | undefined
35
33
  ): boolean => {
36
- if (!filter) return true;
37
- if (!payload) return false;
38
- return Object.entries(filter).every(([k, v]) => payload[k] === v);
34
+ if (!filter) return true;
35
+ if (!payload) return false;
36
+ return Object.entries(filter).every(([k, v]) => payload[k] === v);
39
37
  };
40
38
 
41
39
  const getAvailability = (
42
- availability: StepAvailabilitySpec | undefined,
43
- startedAt: Date | undefined
40
+ availability: StepAvailabilitySpec | undefined,
41
+ startedAt: Date | undefined
44
42
  ): { availableAt?: Date; dueAt?: Date } => {
45
- if (!availability || !startedAt) return {};
46
- const baseTime = startedAt.getTime();
47
- let unlockTime = baseTime;
48
- if (availability.unlockOnDay !== undefined) {
49
- unlockTime =
50
- baseTime + (availability.unlockOnDay - 1) * 24 * 60 * 60 * 1000;
51
- }
52
- if (availability.unlockAfterHours !== undefined) {
53
- unlockTime = baseTime + availability.unlockAfterHours * 60 * 60 * 1000;
54
- }
55
- const availableAt = new Date(unlockTime);
56
- const dueAt =
57
- availability.dueWithinHours !== undefined
58
- ? new Date(
59
- availableAt.getTime() + availability.dueWithinHours * 60 * 60 * 1000
60
- )
61
- : undefined;
62
- return { availableAt, dueAt };
43
+ if (!availability || !startedAt) return {};
44
+ const baseTime = startedAt.getTime();
45
+ let unlockTime = baseTime;
46
+ if (availability.unlockOnDay !== undefined) {
47
+ unlockTime =
48
+ baseTime + (availability.unlockOnDay - 1) * 24 * 60 * 60 * 1000;
49
+ }
50
+ if (availability.unlockAfterHours !== undefined) {
51
+ unlockTime = baseTime + availability.unlockAfterHours * 60 * 60 * 1000;
52
+ }
53
+ const availableAt = new Date(unlockTime);
54
+ const dueAt =
55
+ availability.dueWithinHours !== undefined
56
+ ? new Date(
57
+ availableAt.getTime() + availability.dueWithinHours * 60 * 60 * 1000
58
+ )
59
+ : undefined;
60
+ return { availableAt, dueAt };
63
61
  };
64
62
 
65
63
  const matchesCondition = (
66
- condition: StepCompletionConditionSpec,
67
- event: LearningEvent,
68
- step: StepState
64
+ condition: StepCompletionConditionSpec,
65
+ event: LearningEvent,
66
+ step: StepState
69
67
  ): { matched: boolean; occurrences?: number; masteryCount?: number } => {
70
- if ((condition.kind ?? 'event') === 'event') {
71
- if (condition.eventName !== event.name) return { matched: false };
72
- if (!matchesFilter(condition.payloadFilter, event.payload))
73
- return { matched: false };
74
- return { matched: true };
75
- }
76
- if (condition.kind === 'count') {
77
- if (condition.eventName !== event.name) return { matched: false };
78
- if (!matchesFilter(condition.payloadFilter, event.payload))
79
- return { matched: false };
80
- const occurrences = step.occurrences + 1;
81
- return { matched: occurrences >= condition.atLeast, occurrences };
82
- }
83
- if (condition.kind === 'srs_mastery') {
84
- if (condition.eventName !== event.name) return { matched: false };
85
- if (!matchesFilter(condition.payloadFilter, event.payload))
86
- return { matched: false };
87
- const masteryKey = condition.masteryField ?? 'mastery';
88
- const masteryValue = event.payload?.[masteryKey];
89
- if (typeof masteryValue !== 'number') return { matched: false };
90
- if (masteryValue < condition.minimumMastery) return { matched: false };
91
- const masteryCount = step.masteryCount + 1;
92
- const required = condition.requiredCount ?? 1;
93
- return { matched: masteryCount >= required, masteryCount };
94
- }
95
- if (condition.kind === 'time_window') {
96
- // For this example suite, we treat time_window as a direct match on eventName
97
- if (condition.eventName !== event.name) return { matched: false };
98
- return { matched: true };
99
- }
100
- return { matched: false };
68
+ if ((condition.kind ?? 'event') === 'event') {
69
+ if (condition.eventName !== event.name) return { matched: false };
70
+ if (!matchesFilter(condition.payloadFilter, event.payload))
71
+ return { matched: false };
72
+ return { matched: true };
73
+ }
74
+ if (condition.kind === 'count') {
75
+ if (condition.eventName !== event.name) return { matched: false };
76
+ if (!matchesFilter(condition.payloadFilter, event.payload))
77
+ return { matched: false };
78
+ const occurrences = step.occurrences + 1;
79
+ return { matched: occurrences >= condition.atLeast, occurrences };
80
+ }
81
+ if (condition.kind === 'srs_mastery') {
82
+ if (condition.eventName !== event.name) return { matched: false };
83
+ if (!matchesFilter(condition.payloadFilter, event.payload))
84
+ return { matched: false };
85
+ const masteryKey = condition.masteryField ?? 'mastery';
86
+ const masteryValue = event.payload?.[masteryKey];
87
+ if (typeof masteryValue !== 'number') return { matched: false };
88
+ if (masteryValue < condition.minimumMastery) return { matched: false };
89
+ const masteryCount = step.masteryCount + 1;
90
+ const required = condition.requiredCount ?? 1;
91
+ return { matched: masteryCount >= required, masteryCount };
92
+ }
93
+ if (condition.kind === 'time_window') {
94
+ // For this example suite, we treat time_window as a direct match on eventName
95
+ if (condition.eventName !== event.name) return { matched: false };
96
+ return { matched: true };
97
+ }
98
+ return { matched: false };
101
99
  };
102
100
 
103
101
  function initProgress(track: LearningJourneyTrackSpec): StepState[] {
104
- return track.steps.map((s) => ({
105
- id: s.id,
106
- status: 'PENDING',
107
- occurrences: 0,
108
- masteryCount: 0,
109
- }));
102
+ return track.steps.map((s) => ({
103
+ id: s.id,
104
+ status: 'PENDING',
105
+ occurrences: 0,
106
+ masteryCount: 0,
107
+ }));
110
108
  }
111
109
 
112
110
  function applyEvents(
113
- track: LearningJourneyTrackSpec,
114
- events: LearningEvent[]
111
+ track: LearningJourneyTrackSpec,
112
+ events: LearningEvent[]
115
113
  ): StepState[] {
116
- const steps = initProgress(track);
117
- let startedAt: Date | undefined;
118
- for (const event of events) {
119
- const eventTime = event.occurredAt ?? new Date();
120
- if (!startedAt) startedAt = eventTime;
121
- for (let index = 0; index < track.steps.length; index++) {
122
- const spec = track.steps[index];
123
- const state = steps[index];
124
- if (!spec || !state) continue;
125
- if (state.status === 'COMPLETED') continue;
126
- const { availableAt, dueAt } = getAvailability(
127
- spec.availability,
128
- startedAt
129
- );
130
- state.availableAt = availableAt;
131
- state.dueAt = dueAt;
132
- if (availableAt && eventTime < availableAt) continue;
133
- if (dueAt && eventTime > dueAt) continue;
134
- const res = matchesCondition(spec.completion, event, state);
135
- if (res.occurrences !== undefined) state.occurrences = res.occurrences;
136
- if (res.masteryCount !== undefined) state.masteryCount = res.masteryCount;
137
- if (res.matched) state.status = 'COMPLETED';
138
- }
139
- }
140
- return steps;
114
+ const steps = initProgress(track);
115
+ let startedAt: Date | undefined;
116
+ for (const event of events) {
117
+ const eventTime = event.occurredAt ?? new Date();
118
+ if (!startedAt) startedAt = eventTime;
119
+ for (let index = 0; index < track.steps.length; index++) {
120
+ const spec = track.steps[index];
121
+ const state = steps[index];
122
+ if (!spec || !state) continue;
123
+ if (state.status === 'COMPLETED') continue;
124
+ const { availableAt, dueAt } = getAvailability(
125
+ spec.availability,
126
+ startedAt
127
+ );
128
+ state.availableAt = availableAt;
129
+ state.dueAt = dueAt;
130
+ if (availableAt && eventTime < availableAt) continue;
131
+ if (dueAt && eventTime > dueAt) continue;
132
+ const res = matchesCondition(spec.completion, event, state);
133
+ if (res.occurrences !== undefined) state.occurrences = res.occurrences;
134
+ if (res.masteryCount !== undefined) state.masteryCount = res.masteryCount;
135
+ if (res.matched) state.status = 'COMPLETED';
136
+ }
137
+ }
138
+ return steps;
141
139
  }
142
140
 
143
141
  describe('@contractspec/example.learning-patterns tracks', () => {
144
- it('drills track progresses via session count + mastery', () => {
145
- const events: LearningEvent[] = [
146
- { name: LEARNING_EVENTS.DRILL_SESSION_COMPLETED },
147
- {
148
- name: LEARNING_EVENTS.DRILL_SESSION_COMPLETED,
149
- payload: { accuracyBucket: 'high' },
150
- },
151
- {
152
- name: LEARNING_EVENTS.DRILL_SESSION_COMPLETED,
153
- payload: { accuracyBucket: 'high' },
154
- },
155
- {
156
- name: LEARNING_EVENTS.DRILL_SESSION_COMPLETED,
157
- payload: { accuracyBucket: 'high' },
158
- },
159
- ...Array.from({ length: 5 }).map(() => ({
160
- name: LEARNING_EVENTS.DRILL_CARD_MASTERED,
161
- payload: { skillId: 's1', mastery: 0.9 },
162
- })),
163
- ];
164
- const progress = applyEvents(drillsTrack, events);
165
- expect(progress.every((s) => s.status === 'COMPLETED')).toBeTrue();
166
- });
167
-
168
- it('ambient coach track progresses via shown -> acknowledged -> actionTaken', () => {
169
- const progress = applyEvents(ambientCoachTrack, [
170
- { name: LEARNING_EVENTS.COACH_TIP_SHOWN },
171
- { name: LEARNING_EVENTS.COACH_TIP_ACKNOWLEDGED },
172
- { name: LEARNING_EVENTS.COACH_TIP_ACTION_TAKEN },
173
- ]);
174
- expect(progress.every((s) => s.status === 'COMPLETED')).toBeTrue();
175
- });
176
-
177
- it('quest track respects unlockOnDay availability', () => {
178
- const start = new Date('2026-01-01T10:00:00.000Z');
179
- const day1 = new Date('2026-01-01T12:00:00.000Z');
180
- const day2 = new Date('2026-01-02T12:00:00.000Z');
181
-
182
- // Attempt to complete steps on day1 (only day1 step should unlock)
183
- const p1 = applyEvents(questTrack, [
184
- { name: LEARNING_EVENTS.QUEST_STARTED, occurredAt: start },
185
- { name: LEARNING_EVENTS.QUEST_STEP_COMPLETED, occurredAt: day1 },
186
- ]);
187
- expect(p1[0]?.status).toBe('COMPLETED');
188
- expect(p1[1]?.status).toBe('COMPLETED');
189
- expect(p1[2]?.status).toBe('PENDING'); // day2 step not yet available
190
-
191
- // Now complete on day2
192
- const p2 = applyEvents(questTrack, [
193
- { name: LEARNING_EVENTS.QUEST_STARTED, occurredAt: start },
194
- { name: LEARNING_EVENTS.QUEST_STEP_COMPLETED, occurredAt: day2 },
195
- ]);
196
- expect(p2[2]?.status).toBe('COMPLETED');
197
- });
142
+ it('drills track progresses via session count + mastery', () => {
143
+ const events: LearningEvent[] = [
144
+ { name: LEARNING_EVENTS.DRILL_SESSION_COMPLETED },
145
+ {
146
+ name: LEARNING_EVENTS.DRILL_SESSION_COMPLETED,
147
+ payload: { accuracyBucket: 'high' },
148
+ },
149
+ {
150
+ name: LEARNING_EVENTS.DRILL_SESSION_COMPLETED,
151
+ payload: { accuracyBucket: 'high' },
152
+ },
153
+ {
154
+ name: LEARNING_EVENTS.DRILL_SESSION_COMPLETED,
155
+ payload: { accuracyBucket: 'high' },
156
+ },
157
+ ...Array.from({ length: 5 }).map(() => ({
158
+ name: LEARNING_EVENTS.DRILL_CARD_MASTERED,
159
+ payload: { skillId: 's1', mastery: 0.9 },
160
+ })),
161
+ ];
162
+ const progress = applyEvents(drillsTrack, events);
163
+ expect(progress.every((s) => s.status === 'COMPLETED')).toBeTrue();
164
+ });
165
+
166
+ it('ambient coach track progresses via shown -> acknowledged -> actionTaken', () => {
167
+ const progress = applyEvents(ambientCoachTrack, [
168
+ { name: LEARNING_EVENTS.COACH_TIP_SHOWN },
169
+ { name: LEARNING_EVENTS.COACH_TIP_ACKNOWLEDGED },
170
+ { name: LEARNING_EVENTS.COACH_TIP_ACTION_TAKEN },
171
+ ]);
172
+ expect(progress.every((s) => s.status === 'COMPLETED')).toBeTrue();
173
+ });
174
+
175
+ it('quest track respects unlockOnDay availability', () => {
176
+ const start = new Date('2026-01-01T10:00:00.000Z');
177
+ const day1 = new Date('2026-01-01T12:00:00.000Z');
178
+ const day2 = new Date('2026-01-02T12:00:00.000Z');
179
+
180
+ // Attempt to complete steps on day1 (only day1 step should unlock)
181
+ const p1 = applyEvents(questTrack, [
182
+ { name: LEARNING_EVENTS.QUEST_STARTED, occurredAt: start },
183
+ { name: LEARNING_EVENTS.QUEST_STEP_COMPLETED, occurredAt: day1 },
184
+ ]);
185
+ expect(p1[0]?.status).toBe('COMPLETED');
186
+ expect(p1[1]?.status).toBe('COMPLETED');
187
+ expect(p1[2]?.status).toBe('PENDING'); // day2 step not yet available
188
+
189
+ // Now complete on day2
190
+ const p2 = applyEvents(questTrack, [
191
+ { name: LEARNING_EVENTS.QUEST_STARTED, occurredAt: start },
192
+ { name: LEARNING_EVENTS.QUEST_STEP_COMPLETED, occurredAt: day2 },
193
+ ]);
194
+ expect(p2[2]?.status).toBe('COMPLETED');
195
+ });
198
196
  });
199
197
 
200
198
  describe('@contractspec/example.learning-patterns XP + streak + SRS determinism', () => {
201
- it('XP engine produces deterministic results for streak bonus inputs', () => {
202
- const xp = new XPEngine();
203
- const r1 = xp.calculate({
204
- activity: 'lesson_complete',
205
- score: 90,
206
- attemptNumber: 1,
207
- currentStreak: 7,
208
- });
209
- const r2 = xp.calculate({
210
- activity: 'lesson_complete',
211
- score: 90,
212
- attemptNumber: 1,
213
- currentStreak: 7,
214
- });
215
- expect(r1.totalXp).toBe(r2.totalXp);
216
- expect(r1.totalXp).toBeGreaterThan(0);
217
- });
218
-
219
- it('streak engine increments on consecutive days deterministically', () => {
220
- const streak = new StreakEngine({ timezone: 'UTC' });
221
- const initial = {
222
- currentStreak: 0,
223
- longestStreak: 0,
224
- lastActivityAt: null,
225
- lastActivityDate: null,
226
- freezesRemaining: 0,
227
- freezeUsedAt: null,
228
- };
229
- const day1 = streak.update(
230
- initial,
231
- new Date('2026-01-01T10:00:00.000Z')
232
- ).state;
233
- const day2 = streak.update(
234
- day1,
235
- new Date('2026-01-02T10:00:00.000Z')
236
- ).state;
237
- expect(day2.currentStreak).toBe(2);
238
- });
239
-
240
- it('SRS engine nextReviewAt is deterministic for a fixed now + rating', () => {
241
- const srs = new SRSEngine();
242
- const now = new Date('2026-01-01T00:00:00.000Z');
243
- const state = {
244
- interval: 0,
245
- easeFactor: 2.5,
246
- repetitions: 0,
247
- learningStep: 0,
248
- isGraduated: false,
249
- isRelearning: false,
250
- lapses: 0,
251
- };
252
- const result = srs.calculateNextReview(state, 'GOOD', now);
253
- // default learningSteps are minutes; first GOOD advances to next step (10 minutes)
254
- expect(result.nextReviewAt.toISOString()).toBe('2026-01-01T00:10:00.000Z');
255
- });
199
+ it('XP engine produces deterministic results for streak bonus inputs', () => {
200
+ const xp = new XPEngine();
201
+ const r1 = xp.calculate({
202
+ activity: 'lesson_complete',
203
+ score: 90,
204
+ attemptNumber: 1,
205
+ currentStreak: 7,
206
+ });
207
+ const r2 = xp.calculate({
208
+ activity: 'lesson_complete',
209
+ score: 90,
210
+ attemptNumber: 1,
211
+ currentStreak: 7,
212
+ });
213
+ expect(r1.totalXp).toBe(r2.totalXp);
214
+ expect(r1.totalXp).toBeGreaterThan(0);
215
+ });
216
+
217
+ it('streak engine increments on consecutive days deterministically', () => {
218
+ const streak = new StreakEngine({ timezone: 'UTC' });
219
+ const initial = {
220
+ currentStreak: 0,
221
+ longestStreak: 0,
222
+ lastActivityAt: null,
223
+ lastActivityDate: null,
224
+ freezesRemaining: 0,
225
+ freezeUsedAt: null,
226
+ };
227
+ const day1 = streak.update(
228
+ initial,
229
+ new Date('2026-01-01T10:00:00.000Z')
230
+ ).state;
231
+ const day2 = streak.update(
232
+ day1,
233
+ new Date('2026-01-02T10:00:00.000Z')
234
+ ).state;
235
+ expect(day2.currentStreak).toBe(2);
236
+ });
237
+
238
+ it('SRS engine nextReviewAt is deterministic for a fixed now + rating', () => {
239
+ const srs = new SRSEngine();
240
+ const now = new Date('2026-01-01T00:00:00.000Z');
241
+ const state = {
242
+ interval: 0,
243
+ easeFactor: 2.5,
244
+ repetitions: 0,
245
+ learningStep: 0,
246
+ isGraduated: false,
247
+ isRelearning: false,
248
+ lapses: 0,
249
+ };
250
+ const result = srs.calculateNextReview(state, 'GOOD', now);
251
+ // default learningSteps are minutes; first GOOD advances to next step (10 minutes)
252
+ expect(result.nextReviewAt.toISOString()).toBe('2026-01-01T00:10:00.000Z');
253
+ });
256
254
  });
@@ -2,43 +2,43 @@ import type { LearningJourneyTrackSpec } from '@contractspec/module.learning-jou
2
2
  import { LEARNING_EVENTS } from '../events';
3
3
 
4
4
  export const ambientCoachTrack: LearningJourneyTrackSpec = {
5
- id: 'learning_patterns_ambient_coach_basics',
6
- name: 'Ambient Coach Basics',
7
- description: 'Contextual tips triggered by behavior events.',
8
- targetUserSegment: 'learner',
9
- targetRole: 'individual',
10
- totalXp: 30,
11
- steps: [
12
- {
13
- id: 'tip_shown',
14
- title: 'See a contextual tip',
15
- order: 1,
16
- completion: { kind: 'event', eventName: LEARNING_EVENTS.COACH_TIP_SHOWN },
17
- xpReward: 10,
18
- },
19
- {
20
- id: 'tip_acknowledged',
21
- title: 'Acknowledge a tip',
22
- order: 2,
23
- completion: {
24
- kind: 'event',
25
- eventName: LEARNING_EVENTS.COACH_TIP_ACKNOWLEDGED,
26
- },
27
- xpReward: 10,
28
- },
29
- {
30
- id: 'tip_action_taken',
31
- title: 'Take an action from a tip',
32
- order: 3,
33
- completion: {
34
- kind: 'event',
35
- eventName: LEARNING_EVENTS.COACH_TIP_ACTION_TAKEN,
36
- },
37
- xpReward: 10,
38
- },
39
- ],
5
+ id: 'learning_patterns_ambient_coach_basics',
6
+ name: 'Ambient Coach Basics',
7
+ description: 'Contextual tips triggered by behavior events.',
8
+ targetUserSegment: 'learner',
9
+ targetRole: 'individual',
10
+ totalXp: 30,
11
+ steps: [
12
+ {
13
+ id: 'tip_shown',
14
+ title: 'See a contextual tip',
15
+ order: 1,
16
+ completion: { kind: 'event', eventName: LEARNING_EVENTS.COACH_TIP_SHOWN },
17
+ xpReward: 10,
18
+ },
19
+ {
20
+ id: 'tip_acknowledged',
21
+ title: 'Acknowledge a tip',
22
+ order: 2,
23
+ completion: {
24
+ kind: 'event',
25
+ eventName: LEARNING_EVENTS.COACH_TIP_ACKNOWLEDGED,
26
+ },
27
+ xpReward: 10,
28
+ },
29
+ {
30
+ id: 'tip_action_taken',
31
+ title: 'Take an action from a tip',
32
+ order: 3,
33
+ completion: {
34
+ kind: 'event',
35
+ eventName: LEARNING_EVENTS.COACH_TIP_ACTION_TAKEN,
36
+ },
37
+ xpReward: 10,
38
+ },
39
+ ],
40
40
  };
41
41
 
42
42
  export const ambientCoachTracks: LearningJourneyTrackSpec[] = [
43
- ambientCoachTrack,
43
+ ambientCoachTrack,
44
44
  ];
@@ -2,50 +2,50 @@ import type { LearningJourneyTrackSpec } from '@contractspec/module.learning-jou
2
2
  import { LEARNING_EVENTS } from '../events';
3
3
 
4
4
  export const drillsTrack: LearningJourneyTrackSpec = {
5
- id: 'learning_patterns_drills_basics',
6
- name: 'Drills Basics',
7
- description: 'Short drill sessions with an SRS-style mastery step.',
8
- targetUserSegment: 'learner',
9
- targetRole: 'individual',
10
- totalXp: 50,
11
- steps: [
12
- {
13
- id: 'complete_first_session',
14
- title: 'Complete your first session',
15
- order: 1,
16
- completion: {
17
- kind: 'event',
18
- eventName: LEARNING_EVENTS.DRILL_SESSION_COMPLETED,
19
- },
20
- xpReward: 10,
21
- },
22
- {
23
- id: 'hit_accuracy_threshold',
24
- title: 'Hit high accuracy 3 times',
25
- order: 2,
26
- completion: {
27
- kind: 'count',
28
- eventName: LEARNING_EVENTS.DRILL_SESSION_COMPLETED,
29
- atLeast: 3,
30
- payloadFilter: { accuracyBucket: 'high' },
31
- },
32
- xpReward: 20,
33
- },
34
- {
35
- id: 'master_cards',
36
- title: 'Master 5 cards',
37
- order: 3,
38
- completion: {
39
- kind: 'srs_mastery',
40
- eventName: LEARNING_EVENTS.DRILL_CARD_MASTERED,
41
- minimumMastery: 0.8,
42
- requiredCount: 5,
43
- skillIdField: 'skillId',
44
- masteryField: 'mastery',
45
- },
46
- xpReward: 20,
47
- },
48
- ],
5
+ id: 'learning_patterns_drills_basics',
6
+ name: 'Drills Basics',
7
+ description: 'Short drill sessions with an SRS-style mastery step.',
8
+ targetUserSegment: 'learner',
9
+ targetRole: 'individual',
10
+ totalXp: 50,
11
+ steps: [
12
+ {
13
+ id: 'complete_first_session',
14
+ title: 'Complete your first session',
15
+ order: 1,
16
+ completion: {
17
+ kind: 'event',
18
+ eventName: LEARNING_EVENTS.DRILL_SESSION_COMPLETED,
19
+ },
20
+ xpReward: 10,
21
+ },
22
+ {
23
+ id: 'hit_accuracy_threshold',
24
+ title: 'Hit high accuracy 3 times',
25
+ order: 2,
26
+ completion: {
27
+ kind: 'count',
28
+ eventName: LEARNING_EVENTS.DRILL_SESSION_COMPLETED,
29
+ atLeast: 3,
30
+ payloadFilter: { accuracyBucket: 'high' },
31
+ },
32
+ xpReward: 20,
33
+ },
34
+ {
35
+ id: 'master_cards',
36
+ title: 'Master 5 cards',
37
+ order: 3,
38
+ completion: {
39
+ kind: 'srs_mastery',
40
+ eventName: LEARNING_EVENTS.DRILL_CARD_MASTERED,
41
+ minimumMastery: 0.8,
42
+ requiredCount: 5,
43
+ skillIdField: 'skillId',
44
+ masteryField: 'mastery',
45
+ },
46
+ xpReward: 20,
47
+ },
48
+ ],
49
49
  };
50
50
 
51
51
  export const drillTracks: LearningJourneyTrackSpec[] = [drillsTrack];
@@ -1,3 +1,3 @@
1
- export * from './drills';
2
1
  export * from './ambient-coach';
2
+ export * from './drills';
3
3
  export * from './quests';