@contractspec/example.learning-journey-duo-drills 0.0.0-canary-20260113162409
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 +322 -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 +7 -0
- package/dist/example.d.ts.map +1 -0
- package/dist/example.js +43 -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 +57 -0
- package/src/docs/duo-drills.docblock.ts +33 -0
- package/src/docs/index.ts +1 -0
- package/src/example.ts +31 -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
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@contractspec/example.learning-journey-duo-drills",
|
|
3
|
+
"version": "0.0.0-canary-20260113162409",
|
|
4
|
+
"description": "Drill-based learning journey example with SRS, XP, and streak hooks.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js",
|
|
9
|
+
"./docs": "./dist/docs/index.js",
|
|
10
|
+
"./docs/duo-drills.docblock": "./dist/docs/duo-drills.docblock.js",
|
|
11
|
+
"./example": "./dist/example.js",
|
|
12
|
+
"./track": "./dist/track.js",
|
|
13
|
+
"./*": "./*"
|
|
14
|
+
},
|
|
15
|
+
"scripts": {
|
|
16
|
+
"publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
|
|
17
|
+
"publish:pkg:canary": "bun publish:pkg --tag canary",
|
|
18
|
+
"build": "bun build:types && bun build:bundle",
|
|
19
|
+
"build:bundle": "tsdown",
|
|
20
|
+
"build:types": "tsc --noEmit",
|
|
21
|
+
"dev": "bun build:bundle --watch",
|
|
22
|
+
"clean": "rimraf dist .turbo",
|
|
23
|
+
"lint": "bun lint:fix",
|
|
24
|
+
"lint:fix": "eslint src --fix",
|
|
25
|
+
"lint:check": "eslint src",
|
|
26
|
+
"test": "bun test"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@contractspec/module.learning-journey": "0.0.0-canary-20260113162409",
|
|
30
|
+
"@contractspec/lib.contracts": "0.0.0-canary-20260113162409"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@contractspec/tool.tsdown": "0.0.0-canary-20260113162409",
|
|
34
|
+
"@contractspec/tool.typescript": "0.0.0-canary-20260113162409",
|
|
35
|
+
"tsdown": "^0.19.0",
|
|
36
|
+
"typescript": "^5.9.3"
|
|
37
|
+
},
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"exports": {
|
|
40
|
+
".": "./dist/index.js",
|
|
41
|
+
"./example": "./dist/example.js",
|
|
42
|
+
"./track": "./dist/track.js",
|
|
43
|
+
"./docs": "./dist/docs/index.js",
|
|
44
|
+
"./docs/duo-drills.docblock": "./dist/docs/duo-drills.docblock.js",
|
|
45
|
+
"./*": "./*"
|
|
46
|
+
},
|
|
47
|
+
"registry": "https://registry.npmjs.org/",
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"license": "MIT",
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "https://github.com/lssm-tech/contractspec.git",
|
|
54
|
+
"directory": "packages/examples/learning-journey-duo-drills"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://contractspec.io"
|
|
57
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { DocBlock } from '@contractspec/lib.contracts/docs';
|
|
2
|
+
import { registerDocBlocks } from '@contractspec/lib.contracts/docs';
|
|
3
|
+
|
|
4
|
+
const duoDrillsDocBlocks: DocBlock[] = [
|
|
5
|
+
{
|
|
6
|
+
id: 'docs.learning-journey.duo-drills',
|
|
7
|
+
title: 'Learning Journey — Duo Drills',
|
|
8
|
+
summary:
|
|
9
|
+
'Short drill/SRS example with XP and streak hooks for language, finance, or ContractSpec concept drills.',
|
|
10
|
+
kind: 'reference',
|
|
11
|
+
visibility: 'public',
|
|
12
|
+
route: '/docs/learning-journey/duo-drills',
|
|
13
|
+
tags: ['learning', 'drills', 'srs', 'xp'],
|
|
14
|
+
body: `## Track
|
|
15
|
+
- **Key**: \`drills_language_basics\`
|
|
16
|
+
- **Persona**: learner running quick drills (language/finance/spec concepts)
|
|
17
|
+
- **Goal**: complete first session, maintain high-accuracy sessions, master cards in the first skill
|
|
18
|
+
|
|
19
|
+
## Steps & Conditions
|
|
20
|
+
1) \`complete_first_session\` → event \`drill.session.completed\`
|
|
21
|
+
2) \`reach_accuracy_threshold\` → count 3 sessions with payload \`accuracyBucket: "high"\` (within default window)
|
|
22
|
+
3) \`unlock_new_skill\` → SRS mastery: \`drill.card.mastered\` events with \`mastery >= 0.8\`, count 5 cards
|
|
23
|
+
|
|
24
|
+
XP: 20 + 30 + 40. Streak: daily session completion can be used to drive streak rewards.
|
|
25
|
+
|
|
26
|
+
## Wiring
|
|
27
|
+
- Tracks export from \`@contractspec/example.learning-journey-duo-drills/track\`.
|
|
28
|
+
- Use registry helper \`recordEvent\` to advance steps from drill/session events.
|
|
29
|
+
- SRS mastery events should include payload: \`{ skillId, mastery }\`.`,
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
registerDocBlocks(duoDrillsDocBlocks);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import './duo-drills.docblock';
|
package/src/example.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { defineExample } from '@contractspec/lib.contracts';
|
|
2
|
+
|
|
3
|
+
const example = defineExample({
|
|
4
|
+
meta: {
|
|
5
|
+
key: 'learning-journey-duo-drills',
|
|
6
|
+
version: '1.0.0',
|
|
7
|
+
title: 'Learning Journey — Duo Drills',
|
|
8
|
+
description:
|
|
9
|
+
'Short drill/SRS example with XP and streak hooks for language, finance, or ContractSpec concept drills.',
|
|
10
|
+
kind: 'template',
|
|
11
|
+
visibility: 'public',
|
|
12
|
+
stability: 'experimental',
|
|
13
|
+
owners: ['@platform.core'],
|
|
14
|
+
tags: ['learning', 'drills', 'srs', 'xp'],
|
|
15
|
+
},
|
|
16
|
+
docs: {
|
|
17
|
+
rootDocId: 'docs.learning-journey.duo-drills',
|
|
18
|
+
},
|
|
19
|
+
entrypoints: {
|
|
20
|
+
packageName: '@contractspec/example.learning-journey-duo-drills',
|
|
21
|
+
docs: './docs',
|
|
22
|
+
},
|
|
23
|
+
surfaces: {
|
|
24
|
+
templates: true,
|
|
25
|
+
sandbox: { enabled: true, modes: ['playground', 'markdown'] },
|
|
26
|
+
studio: { enabled: true, installable: true },
|
|
27
|
+
mcp: { enabled: true },
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
export default example;
|
package/src/index.ts
ADDED
|
@@ -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
|
+
];
|