@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/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,3 @@
1
+ export * from './track';
2
+ export { default as example } from './example';
3
+ import './docs';
@@ -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
+ ];
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "extends": "@contractspec/tool.typescript/react-library.json",
3
+ "include": ["src"],
4
+ "exclude": ["node_modules"],
5
+ "compilerOptions": {
6
+ "rootDir": "src",
7
+ "outDir": "dist"
8
+ }
9
+ }
10
+
11
+
12
+
13
+
14
+
15
+
16
+
17
+
18
+