@contractspec/example.learning-patterns 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.
Files changed (49) hide show
  1. package/.turbo/turbo-build$colon$bundle.log +39 -0
  2. package/.turbo/turbo-build.log +40 -0
  3. package/CHANGELOG.md +322 -0
  4. package/LICENSE +21 -0
  5. package/README.md +15 -0
  6. package/dist/docs/index.d.ts +1 -0
  7. package/dist/docs/index.js +1 -0
  8. package/dist/docs/learning-patterns.docblock.d.ts +1 -0
  9. package/dist/docs/learning-patterns.docblock.js +31 -0
  10. package/dist/docs/learning-patterns.docblock.js.map +1 -0
  11. package/dist/events.d.ts +16 -0
  12. package/dist/events.d.ts.map +1 -0
  13. package/dist/events.js +16 -0
  14. package/dist/events.js.map +1 -0
  15. package/dist/example.d.ts +7 -0
  16. package/dist/example.d.ts.map +1 -0
  17. package/dist/example.js +42 -0
  18. package/dist/example.js.map +1 -0
  19. package/dist/index.d.ts +6 -0
  20. package/dist/index.js +9 -0
  21. package/dist/tracks/ambient-coach.d.ts +8 -0
  22. package/dist/tracks/ambient-coach.d.ts.map +1 -0
  23. package/dist/tracks/ambient-coach.js +48 -0
  24. package/dist/tracks/ambient-coach.js.map +1 -0
  25. package/dist/tracks/drills.d.ts +8 -0
  26. package/dist/tracks/drills.d.ts.map +1 -0
  27. package/dist/tracks/drills.js +54 -0
  28. package/dist/tracks/drills.js.map +1 -0
  29. package/dist/tracks/index.d.ts +4 -0
  30. package/dist/tracks/index.js +5 -0
  31. package/dist/tracks/quests.d.ts +8 -0
  32. package/dist/tracks/quests.d.ts.map +1 -0
  33. package/dist/tracks/quests.js +56 -0
  34. package/dist/tracks/quests.js.map +1 -0
  35. package/example.ts +1 -0
  36. package/package.json +65 -0
  37. package/src/docs/index.ts +1 -0
  38. package/src/docs/learning-patterns.docblock.ts +30 -0
  39. package/src/events.ts +16 -0
  40. package/src/example.ts +31 -0
  41. package/src/index.ts +10 -0
  42. package/src/learning-patterns.test.ts +256 -0
  43. package/src/tracks/ambient-coach.ts +44 -0
  44. package/src/tracks/drills.ts +51 -0
  45. package/src/tracks/index.ts +3 -0
  46. package/src/tracks/quests.ts +44 -0
  47. package/tsconfig.json +19 -0
  48. package/tsconfig.tsbuildinfo +1 -0
  49. package/tsdown.config.js +7 -0
@@ -0,0 +1,48 @@
1
+ import { LEARNING_EVENTS } from "../events.js";
2
+
3
+ //#region src/tracks/ambient-coach.ts
4
+ const ambientCoachTrack = {
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: {
17
+ kind: "event",
18
+ eventName: LEARNING_EVENTS.COACH_TIP_SHOWN
19
+ },
20
+ xpReward: 10
21
+ },
22
+ {
23
+ id: "tip_acknowledged",
24
+ title: "Acknowledge a tip",
25
+ order: 2,
26
+ completion: {
27
+ kind: "event",
28
+ eventName: LEARNING_EVENTS.COACH_TIP_ACKNOWLEDGED
29
+ },
30
+ xpReward: 10
31
+ },
32
+ {
33
+ id: "tip_action_taken",
34
+ title: "Take an action from a tip",
35
+ order: 3,
36
+ completion: {
37
+ kind: "event",
38
+ eventName: LEARNING_EVENTS.COACH_TIP_ACTION_TAKEN
39
+ },
40
+ xpReward: 10
41
+ }
42
+ ]
43
+ };
44
+ const ambientCoachTracks = [ambientCoachTrack];
45
+
46
+ //#endregion
47
+ export { ambientCoachTrack, ambientCoachTracks };
48
+ //# sourceMappingURL=ambient-coach.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ambient-coach.js","names":[],"sources":["../../src/tracks/ambient-coach.ts"],"sourcesContent":["import type { LearningJourneyTrackSpec } from '@contractspec/module.learning-journey/track-spec';\nimport { LEARNING_EVENTS } from '../events';\n\nexport const ambientCoachTrack: LearningJourneyTrackSpec = {\n id: 'learning_patterns_ambient_coach_basics',\n name: 'Ambient Coach Basics',\n description: 'Contextual tips triggered by behavior events.',\n targetUserSegment: 'learner',\n targetRole: 'individual',\n totalXp: 30,\n steps: [\n {\n id: 'tip_shown',\n title: 'See a contextual tip',\n order: 1,\n completion: { kind: 'event', eventName: LEARNING_EVENTS.COACH_TIP_SHOWN },\n xpReward: 10,\n },\n {\n id: 'tip_acknowledged',\n title: 'Acknowledge a tip',\n order: 2,\n completion: {\n kind: 'event',\n eventName: LEARNING_EVENTS.COACH_TIP_ACKNOWLEDGED,\n },\n xpReward: 10,\n },\n {\n id: 'tip_action_taken',\n title: 'Take an action from a tip',\n order: 3,\n completion: {\n kind: 'event',\n eventName: LEARNING_EVENTS.COACH_TIP_ACTION_TAKEN,\n },\n xpReward: 10,\n },\n ],\n};\n\nexport const ambientCoachTracks: LearningJourneyTrackSpec[] = [\n ambientCoachTrack,\n];\n"],"mappings":";;;AAGA,MAAa,oBAA8C;CACzD,IAAI;CACJ,MAAM;CACN,aAAa;CACb,mBAAmB;CACnB,YAAY;CACZ,SAAS;CACT,OAAO;EACL;GACE,IAAI;GACJ,OAAO;GACP,OAAO;GACP,YAAY;IAAE,MAAM;IAAS,WAAW,gBAAgB;IAAiB;GACzE,UAAU;GACX;EACD;GACE,IAAI;GACJ,OAAO;GACP,OAAO;GACP,YAAY;IACV,MAAM;IACN,WAAW,gBAAgB;IAC5B;GACD,UAAU;GACX;EACD;GACE,IAAI;GACJ,OAAO;GACP,OAAO;GACP,YAAY;IACV,MAAM;IACN,WAAW,gBAAgB;IAC5B;GACD,UAAU;GACX;EACF;CACF;AAED,MAAa,qBAAiD,CAC5D,kBACD"}
@@ -0,0 +1,8 @@
1
+ import { LearningJourneyTrackSpec } from "@contractspec/module.learning-journey/track-spec";
2
+
3
+ //#region src/tracks/drills.d.ts
4
+ declare const drillsTrack: LearningJourneyTrackSpec;
5
+ declare const drillTracks: LearningJourneyTrackSpec[];
6
+ //#endregion
7
+ export { drillTracks, drillsTrack };
8
+ //# sourceMappingURL=drills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drills.d.ts","names":[],"sources":["../../src/tracks/drills.ts"],"sourcesContent":[],"mappings":";;;cAGa,aAAa;cA+Cb,aAAa"}
@@ -0,0 +1,54 @@
1
+ import { LEARNING_EVENTS } from "../events.js";
2
+
3
+ //#region src/tracks/drills.ts
4
+ const drillsTrack = {
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: .8,
42
+ requiredCount: 5,
43
+ skillIdField: "skillId",
44
+ masteryField: "mastery"
45
+ },
46
+ xpReward: 20
47
+ }
48
+ ]
49
+ };
50
+ const drillTracks = [drillsTrack];
51
+
52
+ //#endregion
53
+ export { drillTracks, drillsTrack };
54
+ //# sourceMappingURL=drills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"drills.js","names":[],"sources":["../../src/tracks/drills.ts"],"sourcesContent":["import type { LearningJourneyTrackSpec } from '@contractspec/module.learning-journey/track-spec';\nimport { LEARNING_EVENTS } from '../events';\n\nexport const drillsTrack: LearningJourneyTrackSpec = {\n id: 'learning_patterns_drills_basics',\n name: 'Drills Basics',\n description: 'Short drill sessions with an SRS-style mastery step.',\n targetUserSegment: 'learner',\n targetRole: 'individual',\n totalXp: 50,\n steps: [\n {\n id: 'complete_first_session',\n title: 'Complete your first session',\n order: 1,\n completion: {\n kind: 'event',\n eventName: LEARNING_EVENTS.DRILL_SESSION_COMPLETED,\n },\n xpReward: 10,\n },\n {\n id: 'hit_accuracy_threshold',\n title: 'Hit high accuracy 3 times',\n order: 2,\n completion: {\n kind: 'count',\n eventName: LEARNING_EVENTS.DRILL_SESSION_COMPLETED,\n atLeast: 3,\n payloadFilter: { accuracyBucket: 'high' },\n },\n xpReward: 20,\n },\n {\n id: 'master_cards',\n title: 'Master 5 cards',\n order: 3,\n completion: {\n kind: 'srs_mastery',\n eventName: LEARNING_EVENTS.DRILL_CARD_MASTERED,\n minimumMastery: 0.8,\n requiredCount: 5,\n skillIdField: 'skillId',\n masteryField: 'mastery',\n },\n xpReward: 20,\n },\n ],\n};\n\nexport const drillTracks: LearningJourneyTrackSpec[] = [drillsTrack];\n"],"mappings":";;;AAGA,MAAa,cAAwC;CACnD,IAAI;CACJ,MAAM;CACN,aAAa;CACb,mBAAmB;CACnB,YAAY;CACZ,SAAS;CACT,OAAO;EACL;GACE,IAAI;GACJ,OAAO;GACP,OAAO;GACP,YAAY;IACV,MAAM;IACN,WAAW,gBAAgB;IAC5B;GACD,UAAU;GACX;EACD;GACE,IAAI;GACJ,OAAO;GACP,OAAO;GACP,YAAY;IACV,MAAM;IACN,WAAW,gBAAgB;IAC3B,SAAS;IACT,eAAe,EAAE,gBAAgB,QAAQ;IAC1C;GACD,UAAU;GACX;EACD;GACE,IAAI;GACJ,OAAO;GACP,OAAO;GACP,YAAY;IACV,MAAM;IACN,WAAW,gBAAgB;IAC3B,gBAAgB;IAChB,eAAe;IACf,cAAc;IACd,cAAc;IACf;GACD,UAAU;GACX;EACF;CACF;AAED,MAAa,cAA0C,CAAC,YAAY"}
@@ -0,0 +1,4 @@
1
+ import { drillTracks, drillsTrack } from "./drills.js";
2
+ import { ambientCoachTrack, ambientCoachTracks } from "./ambient-coach.js";
3
+ import { questTrack, questTracks } from "./quests.js";
4
+ export { ambientCoachTrack, ambientCoachTracks, drillTracks, drillsTrack, questTrack, questTracks };
@@ -0,0 +1,5 @@
1
+ import { drillTracks, drillsTrack } from "./drills.js";
2
+ import { ambientCoachTrack, ambientCoachTracks } from "./ambient-coach.js";
3
+ import { questTrack, questTracks } from "./quests.js";
4
+
5
+ export { ambientCoachTrack, ambientCoachTracks, drillTracks, drillsTrack, questTrack, questTracks };
@@ -0,0 +1,8 @@
1
+ import { LearningJourneyTrackSpec } from "@contractspec/module.learning-journey/track-spec";
2
+
3
+ //#region src/tracks/quests.d.ts
4
+ declare const questTrack: LearningJourneyTrackSpec;
5
+ declare const questTracks: LearningJourneyTrackSpec[];
6
+ //#endregion
7
+ export { questTrack, questTracks };
8
+ //# sourceMappingURL=quests.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quests.d.ts","names":[],"sources":["../../src/tracks/quests.ts"],"sourcesContent":[],"mappings":";;;cAGa,YAAY;cAwCZ,aAAa"}
@@ -0,0 +1,56 @@
1
+ import { LEARNING_EVENTS } from "../events.js";
2
+
3
+ //#region src/tracks/quests.ts
4
+ const questTrack = {
5
+ id: "learning_patterns_quest_7day",
6
+ name: "Quest (7-day)",
7
+ description: "Time-bounded quest with day unlocks.",
8
+ targetUserSegment: "learner",
9
+ targetRole: "individual",
10
+ totalXp: 70,
11
+ steps: [
12
+ {
13
+ id: "day1_start",
14
+ title: "Start the quest",
15
+ order: 1,
16
+ completion: {
17
+ kind: "event",
18
+ eventName: LEARNING_EVENTS.QUEST_STARTED
19
+ },
20
+ xpReward: 10
21
+ },
22
+ {
23
+ id: "day1_complete",
24
+ title: "Complete day 1 step",
25
+ order: 2,
26
+ completion: {
27
+ kind: "event",
28
+ eventName: LEARNING_EVENTS.QUEST_STEP_COMPLETED
29
+ },
30
+ availability: {
31
+ unlockOnDay: 1,
32
+ dueWithinHours: 48
33
+ },
34
+ xpReward: 10
35
+ },
36
+ {
37
+ id: "day2_complete",
38
+ title: "Complete day 2 step",
39
+ order: 3,
40
+ completion: {
41
+ kind: "event",
42
+ eventName: LEARNING_EVENTS.QUEST_STEP_COMPLETED
43
+ },
44
+ availability: {
45
+ unlockOnDay: 2,
46
+ dueWithinHours: 48
47
+ },
48
+ xpReward: 10
49
+ }
50
+ ]
51
+ };
52
+ const questTracks = [questTrack];
53
+
54
+ //#endregion
55
+ export { questTrack, questTracks };
56
+ //# sourceMappingURL=quests.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"quests.js","names":[],"sources":["../../src/tracks/quests.ts"],"sourcesContent":["import type { LearningJourneyTrackSpec } from '@contractspec/module.learning-journey/track-spec';\nimport { LEARNING_EVENTS } from '../events';\n\nexport const questTrack: LearningJourneyTrackSpec = {\n id: 'learning_patterns_quest_7day',\n name: 'Quest (7-day)',\n description: 'Time-bounded quest with day unlocks.',\n targetUserSegment: 'learner',\n targetRole: 'individual',\n totalXp: 70,\n steps: [\n {\n id: 'day1_start',\n title: 'Start the quest',\n order: 1,\n completion: { kind: 'event', eventName: LEARNING_EVENTS.QUEST_STARTED },\n xpReward: 10,\n },\n {\n id: 'day1_complete',\n title: 'Complete day 1 step',\n order: 2,\n completion: {\n kind: 'event',\n eventName: LEARNING_EVENTS.QUEST_STEP_COMPLETED,\n },\n availability: { unlockOnDay: 1, dueWithinHours: 48 },\n xpReward: 10,\n },\n {\n id: 'day2_complete',\n title: 'Complete day 2 step',\n order: 3,\n completion: {\n kind: 'event',\n eventName: LEARNING_EVENTS.QUEST_STEP_COMPLETED,\n },\n availability: { unlockOnDay: 2, dueWithinHours: 48 },\n xpReward: 10,\n },\n ],\n};\n\nexport const questTracks: LearningJourneyTrackSpec[] = [questTrack];\n"],"mappings":";;;AAGA,MAAa,aAAuC;CAClD,IAAI;CACJ,MAAM;CACN,aAAa;CACb,mBAAmB;CACnB,YAAY;CACZ,SAAS;CACT,OAAO;EACL;GACE,IAAI;GACJ,OAAO;GACP,OAAO;GACP,YAAY;IAAE,MAAM;IAAS,WAAW,gBAAgB;IAAe;GACvE,UAAU;GACX;EACD;GACE,IAAI;GACJ,OAAO;GACP,OAAO;GACP,YAAY;IACV,MAAM;IACN,WAAW,gBAAgB;IAC5B;GACD,cAAc;IAAE,aAAa;IAAG,gBAAgB;IAAI;GACpD,UAAU;GACX;EACD;GACE,IAAI;GACJ,OAAO;GACP,OAAO;GACP,YAAY;IACV,MAAM;IACN,WAAW,gBAAgB;IAC5B;GACD,cAAc;IAAE,aAAa;IAAG,gBAAgB;IAAI;GACpD,UAAU;GACX;EACF;CACF;AAED,MAAa,cAA0C,CAAC,WAAW"}
package/example.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from './src/example';
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@contractspec/example.learning-patterns",
3
+ "version": "0.0.0-canary-20260113162409",
4
+ "description": "Example: drills + ambient coach + quests learning patterns, powered by Learning Journey (event-driven, deterministic).",
5
+ "type": "module",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": "./dist/index.js",
9
+ "./docs": "./dist/docs/index.js",
10
+ "./docs/learning-patterns.docblock": "./dist/docs/learning-patterns.docblock.js",
11
+ "./events": "./dist/events.js",
12
+ "./example": "./dist/example.js",
13
+ "./tracks": "./dist/tracks/index.js",
14
+ "./tracks/ambient-coach": "./dist/tracks/ambient-coach.js",
15
+ "./tracks/drills": "./dist/tracks/drills.js",
16
+ "./tracks/quests": "./dist/tracks/quests.js",
17
+ "./*": "./*"
18
+ },
19
+ "scripts": {
20
+ "publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
21
+ "publish:pkg:canary": "bun publish:pkg --tag canary",
22
+ "build": "bun build:types && bun build:bundle",
23
+ "build:bundle": "tsdown",
24
+ "build:types": "tsc --noEmit",
25
+ "dev": "bun build:bundle --watch",
26
+ "clean": "rimraf dist .turbo",
27
+ "lint": "bun lint:fix",
28
+ "lint:fix": "eslint src --fix",
29
+ "lint:check": "eslint src",
30
+ "test": "bun test"
31
+ },
32
+ "dependencies": {
33
+ "@contractspec/lib.contracts": "0.0.0-canary-20260113162409",
34
+ "@contractspec/module.learning-journey": "0.0.0-canary-20260113162409"
35
+ },
36
+ "devDependencies": {
37
+ "@contractspec/tool.tsdown": "0.0.0-canary-20260113162409",
38
+ "@contractspec/tool.typescript": "0.0.0-canary-20260113162409",
39
+ "tsdown": "^0.19.0",
40
+ "typescript": "^5.9.3"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public",
44
+ "exports": {
45
+ ".": "./dist/index.js",
46
+ "./docs": "./dist/docs/index.js",
47
+ "./docs/learning-patterns.docblock": "./dist/docs/learning-patterns.docblock.js",
48
+ "./events": "./dist/events.js",
49
+ "./example": "./dist/example.js",
50
+ "./tracks": "./dist/tracks/index.js",
51
+ "./tracks/ambient-coach": "./dist/tracks/ambient-coach.js",
52
+ "./tracks/drills": "./dist/tracks/drills.js",
53
+ "./tracks/quests": "./dist/tracks/quests.js",
54
+ "./*": "./*"
55
+ },
56
+ "registry": "https://registry.npmjs.org/"
57
+ },
58
+ "license": "MIT",
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "https://github.com/lssm-tech/contractspec.git",
62
+ "directory": "packages/examples/learning-patterns"
63
+ },
64
+ "homepage": "https://contractspec.io"
65
+ }
@@ -0,0 +1 @@
1
+ import './learning-patterns.docblock';
@@ -0,0 +1,30 @@
1
+ import type { DocBlock } from '@contractspec/lib.contracts/docs';
2
+ import { registerDocBlocks } from '@contractspec/lib.contracts/docs';
3
+
4
+ const docBlocks: DocBlock[] = [
5
+ {
6
+ id: 'docs.examples.learning-patterns.goal',
7
+ title: 'Learning Patterns — Goal',
8
+ summary:
9
+ 'Domain-agnostic drills, ambient coaching, and quests built on Learning Journey.',
10
+ kind: 'goal',
11
+ visibility: 'public',
12
+ route: '/docs/examples/learning-patterns/goal',
13
+ tags: ['learning', 'drills', 'quests', 'coaching'],
14
+ body: `## Why it matters
15
+ - Demonstrates multiple learning archetypes without vertical coupling.\n- Progress is event-driven (no client-side hacks).\n- SRS logic is deterministic and testable.`,
16
+ },
17
+ {
18
+ id: 'docs.examples.learning-patterns.reference',
19
+ title: 'Learning Patterns — Reference',
20
+ summary:
21
+ 'Track specs and event names exported by the learning patterns example.',
22
+ kind: 'reference',
23
+ visibility: 'public',
24
+ route: '/docs/examples/learning-patterns',
25
+ tags: ['learning', 'reference'],
26
+ body: `## Tracks\n- Drills + SRS\n- Ambient Coach\n- Quests\n\n## Events\n- drill.*\n- coach.*\n- quest.*`,
27
+ },
28
+ ];
29
+
30
+ registerDocBlocks(docBlocks);
package/src/events.ts ADDED
@@ -0,0 +1,16 @@
1
+ export const LEARNING_EVENTS = {
2
+ DRILL_CARD_ANSWERED: 'drill.card.answered',
3
+ DRILL_SESSION_COMPLETED: 'drill.session.completed',
4
+ DRILL_CARD_MASTERED: 'drill.card.mastered',
5
+
6
+ COACH_TIP_TRIGGERED: 'coach.tip.triggered',
7
+ COACH_TIP_SHOWN: 'coach.tip.shown',
8
+ COACH_TIP_ACKNOWLEDGED: 'coach.tip.acknowledged',
9
+ COACH_TIP_ACTION_TAKEN: 'coach.tip.actionTaken',
10
+
11
+ QUEST_STARTED: 'quest.started',
12
+ QUEST_STEP_COMPLETED: 'quest.step.completed',
13
+ } as const;
14
+
15
+ export type LearningEventName =
16
+ (typeof LEARNING_EVENTS)[keyof typeof LEARNING_EVENTS];
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-patterns',
6
+ version: '1.0.0',
7
+ title: 'Learning Patterns',
8
+ description:
9
+ 'Domain-agnostic learning archetypes implemented as Learning Journey tracks.',
10
+ kind: 'library',
11
+ visibility: 'public',
12
+ stability: 'experimental',
13
+ owners: ['@platform.core'],
14
+ tags: ['learning', 'journey', 'patterns'],
15
+ },
16
+ docs: {
17
+ rootDocId: 'docs.examples.learning-patterns',
18
+ },
19
+ entrypoints: {
20
+ packageName: '@contractspec/example.learning-patterns',
21
+ docs: './docs',
22
+ },
23
+ surfaces: {
24
+ templates: true,
25
+ sandbox: { enabled: true, modes: ['markdown', 'specs'] },
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,10 @@
1
+ /**
2
+ * Learning Patterns Example
3
+ *
4
+ * Domain-agnostic learning archetypes implemented as Learning Journey tracks.
5
+ */
6
+ export * from './events';
7
+ export * from './tracks';
8
+ export { default as example } from './example';
9
+
10
+ import './docs';
@@ -0,0 +1,256 @@
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
+ import { SRSEngine } from '@contractspec/module.learning-journey/engines/srs';
9
+ import { StreakEngine } from '@contractspec/module.learning-journey/engines/streak';
10
+ import { XPEngine } from '@contractspec/module.learning-journey/engines/xp';
11
+
12
+ import { ambientCoachTrack } from './tracks/ambient-coach';
13
+ import { drillsTrack } from './tracks/drills';
14
+ import { questTrack } from './tracks/quests';
15
+ import { LEARNING_EVENTS } from './events';
16
+
17
+ interface LearningEvent {
18
+ name: string;
19
+ payload?: Record<string, unknown>;
20
+ occurredAt?: Date;
21
+ }
22
+
23
+ interface StepState {
24
+ id: string;
25
+ status: 'PENDING' | 'COMPLETED';
26
+ occurrences: number;
27
+ masteryCount: number;
28
+ availableAt?: Date;
29
+ dueAt?: Date;
30
+ }
31
+
32
+ const matchesFilter = (
33
+ filter: Record<string, unknown> | undefined,
34
+ payload: Record<string, unknown> | undefined
35
+ ): boolean => {
36
+ if (!filter) return true;
37
+ if (!payload) return false;
38
+ return Object.entries(filter).every(([k, v]) => payload[k] === v);
39
+ };
40
+
41
+ const getAvailability = (
42
+ availability: StepAvailabilitySpec | undefined,
43
+ startedAt: Date | undefined
44
+ ): { 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 };
63
+ };
64
+
65
+ const matchesCondition = (
66
+ condition: StepCompletionConditionSpec,
67
+ event: LearningEvent,
68
+ step: StepState
69
+ ): { 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 };
101
+ };
102
+
103
+ 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
+ }));
110
+ }
111
+
112
+ function applyEvents(
113
+ track: LearningJourneyTrackSpec,
114
+ events: LearningEvent[]
115
+ ): 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;
141
+ }
142
+
143
+ 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
+ });
198
+ });
199
+
200
+ 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
+ });
256
+ });