@contractspec/example.learning-patterns 3.7.6 → 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.
- package/.turbo/turbo-build.log +3 -3
- package/AGENTS.md +49 -21
- package/README.md +61 -3
- package/dist/browser/index.js +62 -61
- package/dist/browser/tracks/index.js +42 -42
- package/dist/index.d.ts +2 -2
- package/dist/index.js +62 -61
- package/dist/node/index.js +62 -61
- package/dist/node/tracks/index.js +42 -42
- package/dist/tracks/index.d.ts +1 -1
- package/dist/tracks/index.js +42 -42
- package/package.json +5 -5
- package/src/docs/learning-patterns.docblock.ts +22 -22
- package/src/events.ts +10 -10
- package/src/example.ts +25 -25
- package/src/index.ts +2 -2
- package/src/learning-patterns.feature.ts +16 -16
- package/src/learning-patterns.test.ts +216 -218
- package/src/tracks/ambient-coach.ts +36 -36
- package/src/tracks/drills.ts +44 -44
- package/src/tracks/index.ts +1 -1
- package/src/tracks/quests.ts +37 -37
- package/tsconfig.json +7 -17
- package/tsdown.config.js +7 -3
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
name: string;
|
|
17
|
+
payload?: Record<string, unknown>;
|
|
18
|
+
occurredAt?: Date;
|
|
21
19
|
}
|
|
22
20
|
|
|
23
21
|
interface StepState {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
34
|
-
|
|
31
|
+
filter: Record<string, unknown> | undefined,
|
|
32
|
+
payload: Record<string, unknown> | undefined
|
|
35
33
|
): boolean => {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
43
|
-
|
|
40
|
+
availability: StepAvailabilitySpec | undefined,
|
|
41
|
+
startedAt: Date | undefined
|
|
44
42
|
): { availableAt?: Date; dueAt?: Date } => {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
64
|
+
condition: StepCompletionConditionSpec,
|
|
65
|
+
event: LearningEvent,
|
|
66
|
+
step: StepState
|
|
69
67
|
): { matched: boolean; occurrences?: number; masteryCount?: number } => {
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
114
|
-
|
|
111
|
+
track: LearningJourneyTrackSpec,
|
|
112
|
+
events: LearningEvent[]
|
|
115
113
|
): StepState[] {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
43
|
+
ambientCoachTrack,
|
|
44
44
|
];
|
package/src/tracks/drills.ts
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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];
|
package/src/tracks/index.ts
CHANGED