@contractspec/example.learning-journey-duo-drills 1.44.0
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$colon$bundle.log +25 -0
- package/.turbo/turbo-build.log +26 -0
- package/CHANGELOG.md +178 -0
- package/LICENSE +21 -0
- package/README.md +41 -0
- package/dist/docs/duo-drills.docblock.d.ts +1 -0
- package/dist/docs/duo-drills.docblock.js +36 -0
- package/dist/docs/duo-drills.docblock.js.map +1 -0
- package/dist/docs/index.d.ts +1 -0
- package/dist/docs/index.js +1 -0
- package/dist/example.d.ts +33 -0
- package/dist/example.d.ts.map +1 -0
- package/dist/example.js +36 -0
- package/dist/example.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -0
- package/dist/track.d.ts +8 -0
- package/dist/track.d.ts.map +1 -0
- package/dist/track.js +66 -0
- package/dist/track.js.map +1 -0
- package/example.ts +1 -0
- package/package.json +60 -0
- package/src/docs/duo-drills.docblock.ts +33 -0
- package/src/docs/index.ts +1 -0
- package/src/example.ts +24 -0
- package/src/index.ts +3 -0
- package/src/track.test.ts +106 -0
- package/src/track.ts +62 -0
- package/tsconfig.json +18 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/tsdown.config.js +17 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { drillsLanguageBasicsTrack } from './track';
|
|
4
|
+
|
|
5
|
+
interface TestEvent {
|
|
6
|
+
name: string;
|
|
7
|
+
payload?: Record<string, unknown>;
|
|
8
|
+
occurredAt?: Date;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const matchesFilter = (
|
|
12
|
+
filter: Record<string, unknown> | undefined,
|
|
13
|
+
payload: Record<string, unknown> | undefined
|
|
14
|
+
) => {
|
|
15
|
+
if (!filter) return true;
|
|
16
|
+
if (!payload) return false;
|
|
17
|
+
return Object.entries(filter).every(([key, value]) => payload[key] === value);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
interface StepState {
|
|
21
|
+
id: string;
|
|
22
|
+
status: 'PENDING' | 'COMPLETED';
|
|
23
|
+
occurrences: number;
|
|
24
|
+
masteryCount: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('duo drills track', () => {
|
|
28
|
+
it('advances on session completion, accuracy counts, and SRS mastery', () => {
|
|
29
|
+
const events: TestEvent[] = [
|
|
30
|
+
{
|
|
31
|
+
name: 'drill.session.completed',
|
|
32
|
+
payload: { accuracyBucket: 'high' },
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'drill.session.completed',
|
|
36
|
+
payload: { accuracyBucket: 'high' },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'drill.session.completed',
|
|
40
|
+
payload: { accuracyBucket: 'high' },
|
|
41
|
+
},
|
|
42
|
+
...Array.from({ length: 5 }).map<TestEvent>(() => ({
|
|
43
|
+
name: 'drill.card.mastered',
|
|
44
|
+
payload: { skillId: 'language_basics', mastery: 0.9 },
|
|
45
|
+
})),
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const progress: StepState[] =
|
|
49
|
+
drillsLanguageBasicsTrack.steps.map<StepState>((step) => ({
|
|
50
|
+
id: step.id,
|
|
51
|
+
status: 'PENDING',
|
|
52
|
+
occurrences: 0,
|
|
53
|
+
masteryCount: 0,
|
|
54
|
+
}));
|
|
55
|
+
|
|
56
|
+
events.forEach((event) => {
|
|
57
|
+
drillsLanguageBasicsTrack.steps.forEach((stepSpec, index) => {
|
|
58
|
+
const step = progress[index];
|
|
59
|
+
if (!step || step.status === 'COMPLETED') return;
|
|
60
|
+
const completion = stepSpec.completion;
|
|
61
|
+
if ((completion.kind ?? 'event') === 'event') {
|
|
62
|
+
if (completion.eventName !== event.name) return;
|
|
63
|
+
if (
|
|
64
|
+
matchesFilter(
|
|
65
|
+
completion.payloadFilter,
|
|
66
|
+
event.payload as Record<string, unknown> | undefined
|
|
67
|
+
)
|
|
68
|
+
) {
|
|
69
|
+
step.status = 'COMPLETED';
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (completion.kind === 'count') {
|
|
74
|
+
if (
|
|
75
|
+
completion.eventName === event.name &&
|
|
76
|
+
matchesFilter(
|
|
77
|
+
completion.payloadFilter,
|
|
78
|
+
event.payload as Record<string, unknown> | undefined
|
|
79
|
+
)
|
|
80
|
+
) {
|
|
81
|
+
step.occurrences = step.occurrences + 1;
|
|
82
|
+
if (step.occurrences >= completion.atLeast) {
|
|
83
|
+
step.status = 'COMPLETED';
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (completion.kind === 'srs_mastery') {
|
|
89
|
+
if (completion.eventName !== event.name) return;
|
|
90
|
+
if (!matchesFilter(completion.payloadFilter, event.payload)) return;
|
|
91
|
+
const masteryValue = (
|
|
92
|
+
event.payload as Record<string, unknown> | undefined
|
|
93
|
+
)?.[completion.masteryField ?? 'mastery'];
|
|
94
|
+
if (typeof masteryValue !== 'number') return;
|
|
95
|
+
if (masteryValue < completion.minimumMastery) return;
|
|
96
|
+
step.masteryCount = step.masteryCount + 1;
|
|
97
|
+
if (step.masteryCount >= (completion.requiredCount ?? 1)) {
|
|
98
|
+
step.status = 'COMPLETED';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(progress.every((s) => s.status === 'COMPLETED')).toBeTrue();
|
|
105
|
+
});
|
|
106
|
+
});
|
package/src/track.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { LearningJourneyTrackSpec } from '@contractspec/module.learning-journey/track-spec';
|
|
2
|
+
|
|
3
|
+
export const drillsLanguageBasicsTrack: LearningJourneyTrackSpec = {
|
|
4
|
+
id: 'drills_language_basics',
|
|
5
|
+
name: 'Language Basics Drills',
|
|
6
|
+
description:
|
|
7
|
+
'Short SRS-driven drills to master a first skill, modeled after Duolingo-style sessions.',
|
|
8
|
+
targetUserSegment: 'learner',
|
|
9
|
+
targetRole: 'individual',
|
|
10
|
+
totalXp: 50,
|
|
11
|
+
completionRewards: { xpBonus: 25 },
|
|
12
|
+
steps: [
|
|
13
|
+
{
|
|
14
|
+
id: 'complete_first_session',
|
|
15
|
+
title: 'Complete first drill session',
|
|
16
|
+
description: 'Finish a drill session to get started.',
|
|
17
|
+
order: 1,
|
|
18
|
+
completion: {
|
|
19
|
+
kind: 'event',
|
|
20
|
+
eventName: 'drill.session.completed',
|
|
21
|
+
},
|
|
22
|
+
xpReward: 20,
|
|
23
|
+
metadata: { surface: 'drills' },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: 'reach_accuracy_threshold',
|
|
27
|
+
title: 'Hit high accuracy in sessions',
|
|
28
|
+
description: 'Achieve three high-accuracy sessions to build confidence.',
|
|
29
|
+
order: 2,
|
|
30
|
+
completion: {
|
|
31
|
+
kind: 'count',
|
|
32
|
+
eventName: 'drill.session.completed',
|
|
33
|
+
atLeast: 3,
|
|
34
|
+
payloadFilter: { accuracyBucket: 'high' },
|
|
35
|
+
},
|
|
36
|
+
xpReward: 30,
|
|
37
|
+
metadata: { metric: 'accuracy', target: '>=85%' },
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'unlock_new_skill',
|
|
41
|
+
title: 'Master core cards in first skill',
|
|
42
|
+
description:
|
|
43
|
+
'Reach mastery on at least five cards in the first skill to unlock the next one.',
|
|
44
|
+
order: 3,
|
|
45
|
+
completion: {
|
|
46
|
+
kind: 'srs_mastery',
|
|
47
|
+
eventName: 'drill.card.mastered',
|
|
48
|
+
minimumMastery: 0.8,
|
|
49
|
+
requiredCount: 5,
|
|
50
|
+
skillIdField: 'skillId',
|
|
51
|
+
masteryField: 'mastery',
|
|
52
|
+
payloadFilter: { skillId: 'language_basics' },
|
|
53
|
+
},
|
|
54
|
+
xpReward: 40,
|
|
55
|
+
metadata: { surface: 'srs', skill: 'language_basics' },
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export const drillTracks: LearningJourneyTrackSpec[] = [
|
|
61
|
+
drillsLanguageBasicsTrack,
|
|
62
|
+
];
|