@contractspec/example.learning-journey-registry 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.
Files changed (63) hide show
  1. package/.turbo/turbo-build$colon$bundle.log +50 -0
  2. package/.turbo/turbo-build.log +51 -0
  3. package/CHANGELOG.md +348 -0
  4. package/LICENSE +21 -0
  5. package/README.md +25 -0
  6. package/dist/api-types.d.ts +41 -0
  7. package/dist/api-types.d.ts.map +1 -0
  8. package/dist/api-types.js +0 -0
  9. package/dist/api.d.ts +13 -0
  10. package/dist/api.d.ts.map +1 -0
  11. package/dist/api.js +171 -0
  12. package/dist/api.js.map +1 -0
  13. package/dist/docs/index.d.ts +1 -0
  14. package/dist/docs/index.js +1 -0
  15. package/dist/docs/learning-journey-registry.docblock.d.ts +1 -0
  16. package/dist/docs/learning-journey-registry.docblock.js +38 -0
  17. package/dist/docs/learning-journey-registry.docblock.js.map +1 -0
  18. package/dist/example.d.ts +33 -0
  19. package/dist/example.d.ts.map +1 -0
  20. package/dist/example.js +35 -0
  21. package/dist/example.js.map +1 -0
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.js +10 -0
  24. package/dist/learning-journey-registry.feature.d.ts +12 -0
  25. package/dist/learning-journey-registry.feature.d.ts.map +1 -0
  26. package/dist/learning-journey-registry.feature.js +68 -0
  27. package/dist/learning-journey-registry.feature.js.map +1 -0
  28. package/dist/presentations/index.d.ts +10 -0
  29. package/dist/presentations/index.d.ts.map +1 -0
  30. package/dist/presentations/index.js +71 -0
  31. package/dist/presentations/index.js.map +1 -0
  32. package/dist/progress-store.d.ts +11 -0
  33. package/dist/progress-store.d.ts.map +1 -0
  34. package/dist/progress-store.js +31 -0
  35. package/dist/progress-store.js.map +1 -0
  36. package/dist/tracks.d.ts +40 -0
  37. package/dist/tracks.d.ts.map +1 -0
  38. package/dist/tracks.js +48 -0
  39. package/dist/tracks.js.map +1 -0
  40. package/dist/ui/LearningMiniApp.d.ts +24 -0
  41. package/dist/ui/LearningMiniApp.d.ts.map +1 -0
  42. package/dist/ui/LearningMiniApp.js +80 -0
  43. package/dist/ui/LearningMiniApp.js.map +1 -0
  44. package/dist/ui/index.d.ts +2 -0
  45. package/dist/ui/index.js +3 -0
  46. package/example.ts +1 -0
  47. package/package.json +89 -0
  48. package/src/api-types.ts +43 -0
  49. package/src/api.test.ts +46 -0
  50. package/src/api.ts +301 -0
  51. package/src/docs/index.ts +1 -0
  52. package/src/docs/learning-journey-registry.docblock.ts +36 -0
  53. package/src/example.ts +24 -0
  54. package/src/index.ts +8 -0
  55. package/src/learning-journey-registry.feature.ts +64 -0
  56. package/src/presentations/index.ts +69 -0
  57. package/src/progress-store.ts +39 -0
  58. package/src/tracks.ts +91 -0
  59. package/src/ui/LearningMiniApp.tsx +121 -0
  60. package/src/ui/index.ts +5 -0
  61. package/tsconfig.json +9 -0
  62. package/tsconfig.tsbuildinfo +1 -0
  63. package/tsdown.config.js +17 -0
package/dist/tracks.js ADDED
@@ -0,0 +1,48 @@
1
+ import { crmLearningTracks } from "@contractspec/example.learning-journey-crm-onboarding/track";
2
+ import { drillTracks } from "@contractspec/example.learning-journey-duo-drills/track";
3
+ import { ambientCoachTracks } from "@contractspec/example.learning-journey-ambient-coach/track";
4
+ import { questTracks } from "@contractspec/example.learning-journey-quest-challenges/track";
5
+ import { platformLearningTracks } from "@contractspec/example.learning-journey-platform-tour/track";
6
+ import { studioLearningTracks } from "@contractspec/example.learning-journey-studio-onboarding/track";
7
+
8
+ //#region src/tracks.ts
9
+ const mapStep = (step) => ({
10
+ id: step.id,
11
+ title: step.title,
12
+ description: step.description,
13
+ completionEvent: step.completion.eventName,
14
+ completionCondition: step.completion,
15
+ xpReward: step.xpReward,
16
+ isRequired: step.isRequired,
17
+ canSkip: step.canSkip,
18
+ actionUrl: step.actionUrl,
19
+ actionLabel: step.actionLabel,
20
+ availability: step.availability,
21
+ metadata: step.metadata
22
+ });
23
+ const mapTrackSpecToDto = (track) => ({
24
+ id: track.id,
25
+ name: track.name,
26
+ description: track.description,
27
+ productId: track.productId,
28
+ targetUserSegment: track.targetUserSegment,
29
+ targetRole: track.targetRole,
30
+ totalXp: track.totalXp,
31
+ streakRule: track.streakRule,
32
+ completionRewards: track.completionRewards,
33
+ steps: track.steps.map(mapStep),
34
+ metadata: track.metadata
35
+ });
36
+ const learningJourneyTracks = [
37
+ ...studioLearningTracks,
38
+ ...platformLearningTracks,
39
+ ...crmLearningTracks,
40
+ ...drillTracks,
41
+ ...ambientCoachTracks,
42
+ ...questTracks
43
+ ];
44
+ const onboardingTrackCatalog = learningJourneyTracks.map(mapTrackSpecToDto);
45
+
46
+ //#endregion
47
+ export { crmLearningTracks, learningJourneyTracks, mapStep, mapTrackSpecToDto, onboardingTrackCatalog, platformLearningTracks, studioLearningTracks };
48
+ //# sourceMappingURL=tracks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracks.js","names":["learningJourneyTracks: LearningJourneyTrackSpec[]","onboardingTrackCatalog: OnboardingTrackDto[]"],"sources":["../src/tracks.ts"],"sourcesContent":["import type {\n LearningJourneyStepSpec,\n LearningJourneyTrackSpec,\n StepAvailabilitySpec,\n StepCompletionConditionSpec,\n} from '@contractspec/module.learning-journey/track-spec';\nimport { crmLearningTracks } from '@contractspec/example.learning-journey-crm-onboarding/track';\nimport { drillTracks } from '@contractspec/example.learning-journey-duo-drills/track';\nimport { ambientCoachTracks } from '@contractspec/example.learning-journey-ambient-coach/track';\nimport { questTracks } from '@contractspec/example.learning-journey-quest-challenges/track';\nimport { platformLearningTracks } from '@contractspec/example.learning-journey-platform-tour/track';\nimport { studioLearningTracks } from '@contractspec/example.learning-journey-studio-onboarding/track';\n\nexport interface OnboardingStepDto {\n id: string;\n title: string;\n description?: string;\n completionEvent: string;\n completionCondition?: StepCompletionConditionSpec;\n xpReward?: number;\n isRequired?: boolean;\n canSkip?: boolean;\n actionUrl?: string;\n actionLabel?: string;\n availability?: StepAvailabilitySpec;\n metadata?: Record<string, unknown>;\n}\n\nexport interface OnboardingTrackDto {\n id: string;\n name: string;\n description?: string;\n productId?: string;\n targetUserSegment?: string;\n targetRole?: string;\n totalXp?: number;\n streakRule?: LearningJourneyTrackSpec['streakRule'];\n completionRewards?: LearningJourneyTrackSpec['completionRewards'];\n steps: OnboardingStepDto[];\n metadata?: Record<string, unknown>;\n}\n\nconst mapStep = (step: LearningJourneyStepSpec): OnboardingStepDto => ({\n id: step.id,\n title: step.title,\n description: step.description,\n completionEvent: step.completion.eventName,\n completionCondition: step.completion,\n xpReward: step.xpReward,\n isRequired: step.isRequired,\n canSkip: step.canSkip,\n actionUrl: step.actionUrl,\n actionLabel: step.actionLabel,\n availability: step.availability,\n metadata: step.metadata,\n});\n\nexport const mapTrackSpecToDto = (\n track: LearningJourneyTrackSpec\n): OnboardingTrackDto => ({\n id: track.id,\n name: track.name,\n description: track.description,\n productId: track.productId,\n targetUserSegment: track.targetUserSegment,\n targetRole: track.targetRole,\n totalXp: track.totalXp,\n streakRule: track.streakRule,\n completionRewards: track.completionRewards,\n steps: track.steps.map(mapStep),\n metadata: track.metadata,\n});\n\nexport const learningJourneyTracks: LearningJourneyTrackSpec[] = [\n ...studioLearningTracks,\n ...platformLearningTracks,\n ...crmLearningTracks,\n ...drillTracks,\n ...ambientCoachTracks,\n ...questTracks,\n];\n\nexport const onboardingTrackCatalog: OnboardingTrackDto[] =\n learningJourneyTracks.map(mapTrackSpecToDto);\n\nexport {\n studioLearningTracks,\n platformLearningTracks,\n crmLearningTracks,\n mapStep,\n};\n"],"mappings":";;;;;;;;AA0CA,MAAM,WAAW,UAAsD;CACrE,IAAI,KAAK;CACT,OAAO,KAAK;CACZ,aAAa,KAAK;CAClB,iBAAiB,KAAK,WAAW;CACjC,qBAAqB,KAAK;CAC1B,UAAU,KAAK;CACf,YAAY,KAAK;CACjB,SAAS,KAAK;CACd,WAAW,KAAK;CAChB,aAAa,KAAK;CAClB,cAAc,KAAK;CACnB,UAAU,KAAK;CAChB;AAED,MAAa,qBACX,WACwB;CACxB,IAAI,MAAM;CACV,MAAM,MAAM;CACZ,aAAa,MAAM;CACnB,WAAW,MAAM;CACjB,mBAAmB,MAAM;CACzB,YAAY,MAAM;CAClB,SAAS,MAAM;CACf,YAAY,MAAM;CAClB,mBAAmB,MAAM;CACzB,OAAO,MAAM,MAAM,IAAI,QAAQ;CAC/B,UAAU,MAAM;CACjB;AAED,MAAaA,wBAAoD;CAC/D,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACH,GAAG;CACJ;AAED,MAAaC,yBACX,sBAAsB,IAAI,kBAAkB"}
@@ -0,0 +1,24 @@
1
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
2
+ import { LearningView } from "@contractspec/example.learning-journey-ui-shared";
3
+
4
+ //#region src/ui/LearningMiniApp.d.ts
5
+ /** Template IDs that map to learning journey tracks */
6
+ type LearningTemplateId = 'learning-journey-duo-drills' | 'learning-journey-quest-challenges' | 'learning-journey-studio-onboarding' | 'learning-journey-platform-tour' | 'learning-journey-ambient-coach' | 'learning-journey-crm-onboarding';
7
+ interface LearningMiniAppProps {
8
+ templateId: string;
9
+ initialView?: LearningView;
10
+ onViewChange?: (view: LearningView) => void;
11
+ }
12
+ /** Router component that picks the correct mini-app based on template ID */
13
+ declare function LearningMiniApp({
14
+ templateId,
15
+ initialView,
16
+ onViewChange
17
+ }: LearningMiniAppProps): react_jsx_runtime0.JSX.Element;
18
+ /** Check if a template ID is a learning journey template */
19
+ declare function isLearningTemplate(templateId: string): templateId is LearningTemplateId;
20
+ /** Get all learning template IDs */
21
+ declare function getLearningTemplateIds(): LearningTemplateId[];
22
+ //#endregion
23
+ export { LearningMiniApp, getLearningTemplateIds, isLearningTemplate };
24
+ //# sourceMappingURL=LearningMiniApp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LearningMiniApp.d.ts","names":[],"sources":["../../src/ui/LearningMiniApp.tsx"],"sourcesContent":[],"mappings":";;;;;KAUK,kBAAA;UA+BK,oBAAA;EA/BL,UAAA,EAAA,MAAA;EA+BK,WAAA,CAAA,EAEM,YAFc;EAOd,YAAA,CAAA,EAAA,CAAA,IAAe,EAJP,YAIO,EAAA,GAAA,IAAA;;;AAG7B,iBAHc,eAAA,CAGd;EAAA,UAAA;EAAA,WAAA;EAAA;AAAA,CAAA,EACC,oBADD,CAAA,EACqB,kBAAA,CAAA,GAAA,CAAA,OADrB;;AACqB,iBA2DP,kBAAA,CA3DO,UAAA,EAAA,MAAA,CAAA,EAAA,UAAA,IA6DN,kBA7DM;;AA2DP,iBAOA,sBAAA,CAAA,CALC,EAKyB,kBALP,EAAA"}
@@ -0,0 +1,80 @@
1
+ 'use client';
2
+
3
+ import { learningJourneyTracks } from "../tracks.js";
4
+ import { useMemo } from "react";
5
+ import { GamifiedMiniApp } from "@contractspec/example.learning-journey-ui-gamified";
6
+ import { OnboardingMiniApp } from "@contractspec/example.learning-journey-ui-onboarding";
7
+ import { CoachingMiniApp } from "@contractspec/example.learning-journey-ui-coaching";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+
10
+ //#region src/ui/LearningMiniApp.tsx
11
+ /** Map template IDs to track IDs */
12
+ const TEMPLATE_TO_TRACK = {
13
+ "learning-journey-duo-drills": "drills_language_basics",
14
+ "learning-journey-quest-challenges": "money_reset_7day",
15
+ "learning-journey-studio-onboarding": "studio_getting_started",
16
+ "learning-journey-platform-tour": "platform_tour",
17
+ "learning-journey-ambient-coach": "money_ambient_coach",
18
+ "learning-journey-crm-onboarding": "crm_first_win"
19
+ };
20
+ /** Map template IDs to mini-app type */
21
+ const TEMPLATE_TO_APP_TYPE = {
22
+ "learning-journey-duo-drills": "gamified",
23
+ "learning-journey-quest-challenges": "gamified",
24
+ "learning-journey-studio-onboarding": "onboarding",
25
+ "learning-journey-platform-tour": "onboarding",
26
+ "learning-journey-ambient-coach": "coaching",
27
+ "learning-journey-crm-onboarding": "coaching"
28
+ };
29
+ /** Router component that picks the correct mini-app based on template ID */
30
+ function LearningMiniApp({ templateId, initialView = "overview", onViewChange }) {
31
+ const track = useMemo(() => {
32
+ const trackId = TEMPLATE_TO_TRACK[templateId];
33
+ if (!trackId) return null;
34
+ return learningJourneyTracks.find((t) => t.id === trackId);
35
+ }, [templateId]);
36
+ const appType = TEMPLATE_TO_APP_TYPE[templateId];
37
+ if (!track) return /* @__PURE__ */ jsx("div", {
38
+ className: "rounded-lg border border-amber-500/50 bg-amber-500/10 p-6 text-center",
39
+ children: /* @__PURE__ */ jsxs("p", {
40
+ className: "text-amber-500",
41
+ children: ["Unknown learning template: ", templateId]
42
+ })
43
+ });
44
+ switch (appType) {
45
+ case "gamified": return /* @__PURE__ */ jsx(GamifiedMiniApp, {
46
+ track,
47
+ initialView,
48
+ onViewChange
49
+ });
50
+ case "onboarding": return /* @__PURE__ */ jsx(OnboardingMiniApp, {
51
+ track,
52
+ initialView,
53
+ onViewChange
54
+ });
55
+ case "coaching": return /* @__PURE__ */ jsx(CoachingMiniApp, {
56
+ track,
57
+ initialView,
58
+ onViewChange
59
+ });
60
+ default: return /* @__PURE__ */ jsx("div", {
61
+ className: "rounded-lg border border-red-500/50 bg-red-500/10 p-6 text-center",
62
+ children: /* @__PURE__ */ jsxs("p", {
63
+ className: "text-red-500",
64
+ children: ["Unknown app type for template: ", templateId]
65
+ })
66
+ });
67
+ }
68
+ }
69
+ /** Check if a template ID is a learning journey template */
70
+ function isLearningTemplate(templateId) {
71
+ return templateId in TEMPLATE_TO_TRACK;
72
+ }
73
+ /** Get all learning template IDs */
74
+ function getLearningTemplateIds() {
75
+ return Object.keys(TEMPLATE_TO_TRACK);
76
+ }
77
+
78
+ //#endregion
79
+ export { LearningMiniApp, getLearningTemplateIds, isLearningTemplate };
80
+ //# sourceMappingURL=LearningMiniApp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LearningMiniApp.js","names":["TEMPLATE_TO_TRACK: Record<LearningTemplateId, string>","TEMPLATE_TO_APP_TYPE: Record<\n LearningTemplateId,\n 'gamified' | 'onboarding' | 'coaching'\n>"],"sources":["../../src/ui/LearningMiniApp.tsx"],"sourcesContent":["'use client';\n\nimport { useMemo } from 'react';\nimport { GamifiedMiniApp } from '@contractspec/example.learning-journey-ui-gamified';\nimport { OnboardingMiniApp } from '@contractspec/example.learning-journey-ui-onboarding';\nimport { CoachingMiniApp } from '@contractspec/example.learning-journey-ui-coaching';\nimport type { LearningView } from '@contractspec/example.learning-journey-ui-shared';\nimport { learningJourneyTracks } from '../tracks';\n\n/** Template IDs that map to learning journey tracks */\ntype LearningTemplateId =\n | 'learning-journey-duo-drills'\n | 'learning-journey-quest-challenges'\n | 'learning-journey-studio-onboarding'\n | 'learning-journey-platform-tour'\n | 'learning-journey-ambient-coach'\n | 'learning-journey-crm-onboarding';\n\n/** Map template IDs to track IDs */\nconst TEMPLATE_TO_TRACK: Record<LearningTemplateId, string> = {\n 'learning-journey-duo-drills': 'drills_language_basics',\n 'learning-journey-quest-challenges': 'money_reset_7day',\n 'learning-journey-studio-onboarding': 'studio_getting_started',\n 'learning-journey-platform-tour': 'platform_tour',\n 'learning-journey-ambient-coach': 'money_ambient_coach',\n 'learning-journey-crm-onboarding': 'crm_first_win',\n};\n\n/** Map template IDs to mini-app type */\nconst TEMPLATE_TO_APP_TYPE: Record<\n LearningTemplateId,\n 'gamified' | 'onboarding' | 'coaching'\n> = {\n 'learning-journey-duo-drills': 'gamified',\n 'learning-journey-quest-challenges': 'gamified',\n 'learning-journey-studio-onboarding': 'onboarding',\n 'learning-journey-platform-tour': 'onboarding',\n 'learning-journey-ambient-coach': 'coaching',\n 'learning-journey-crm-onboarding': 'coaching',\n};\n\ninterface LearningMiniAppProps {\n templateId: string;\n initialView?: LearningView;\n onViewChange?: (view: LearningView) => void;\n}\n\n/** Router component that picks the correct mini-app based on template ID */\nexport function LearningMiniApp({\n templateId,\n initialView = 'overview',\n onViewChange,\n}: LearningMiniAppProps) {\n // Find the track for this template\n const track = useMemo(() => {\n const trackId = TEMPLATE_TO_TRACK[templateId as LearningTemplateId];\n if (!trackId) return null;\n return learningJourneyTracks.find((t) => t.id === trackId);\n }, [templateId]);\n\n // Determine app type\n const appType = TEMPLATE_TO_APP_TYPE[templateId as LearningTemplateId];\n\n if (!track) {\n return (\n <div className=\"rounded-lg border border-amber-500/50 bg-amber-500/10 p-6 text-center\">\n <p className=\"text-amber-500\">\n Unknown learning template: {templateId}\n </p>\n </div>\n );\n }\n\n // Render the appropriate mini-app\n switch (appType) {\n case 'gamified':\n return (\n <GamifiedMiniApp\n track={track}\n initialView={initialView}\n onViewChange={onViewChange}\n />\n );\n case 'onboarding':\n return (\n <OnboardingMiniApp\n track={track}\n initialView={initialView}\n onViewChange={onViewChange}\n />\n );\n case 'coaching':\n return (\n <CoachingMiniApp\n track={track}\n initialView={initialView}\n onViewChange={onViewChange}\n />\n );\n default:\n return (\n <div className=\"rounded-lg border border-red-500/50 bg-red-500/10 p-6 text-center\">\n <p className=\"text-red-500\">\n Unknown app type for template: {templateId}\n </p>\n </div>\n );\n }\n}\n\n/** Check if a template ID is a learning journey template */\nexport function isLearningTemplate(\n templateId: string\n): templateId is LearningTemplateId {\n return templateId in TEMPLATE_TO_TRACK;\n}\n\n/** Get all learning template IDs */\nexport function getLearningTemplateIds(): LearningTemplateId[] {\n return Object.keys(TEMPLATE_TO_TRACK) as LearningTemplateId[];\n}\n"],"mappings":";;;;;;;;;;;AAmBA,MAAMA,oBAAwD;CAC5D,+BAA+B;CAC/B,qCAAqC;CACrC,sCAAsC;CACtC,kCAAkC;CAClC,kCAAkC;CAClC,mCAAmC;CACpC;;AAGD,MAAMC,uBAGF;CACF,+BAA+B;CAC/B,qCAAqC;CACrC,sCAAsC;CACtC,kCAAkC;CAClC,kCAAkC;CAClC,mCAAmC;CACpC;;AASD,SAAgB,gBAAgB,EAC9B,YACA,cAAc,YACd,gBACuB;CAEvB,MAAM,QAAQ,cAAc;EAC1B,MAAM,UAAU,kBAAkB;AAClC,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,sBAAsB,MAAM,MAAM,EAAE,OAAO,QAAQ;IACzD,CAAC,WAAW,CAAC;CAGhB,MAAM,UAAU,qBAAqB;AAErC,KAAI,CAAC,MACH,QACE,oBAAC;EAAI,WAAU;YACb,qBAAC;GAAE,WAAU;cAAiB,+BACA;IAC1B;GACA;AAKV,SAAQ,SAAR;EACE,KAAK,WACH,QACE,oBAAC;GACQ;GACM;GACC;IACd;EAEN,KAAK,aACH,QACE,oBAAC;GACQ;GACM;GACC;IACd;EAEN,KAAK,WACH,QACE,oBAAC;GACQ;GACM;GACC;IACd;EAEN,QACE,QACE,oBAAC;GAAI,WAAU;aACb,qBAAC;IAAE,WAAU;eAAe,mCACM;KAC9B;IACA;;;;AAMd,SAAgB,mBACd,YACkC;AAClC,QAAO,cAAc;;;AAIvB,SAAgB,yBAA+C;AAC7D,QAAO,OAAO,KAAK,kBAAkB"}
@@ -0,0 +1,2 @@
1
+ import { LearningMiniApp, getLearningTemplateIds, isLearningTemplate } from "./LearningMiniApp.js";
2
+ export { LearningMiniApp, getLearningTemplateIds, isLearningTemplate };
@@ -0,0 +1,3 @@
1
+ import { LearningMiniApp, getLearningTemplateIds, isLearningTemplate } from "./LearningMiniApp.js";
2
+
3
+ export { LearningMiniApp, getLearningTemplateIds, isLearningTemplate };
package/example.ts ADDED
@@ -0,0 +1 @@
1
+ export { default } from './src/example';
package/package.json ADDED
@@ -0,0 +1,89 @@
1
+ {
2
+ "name": "@contractspec/example.learning-journey-registry",
3
+ "version": "1.44.0",
4
+ "description": "Registry that aggregates learning journey example tracks.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./api": "./dist/api.js",
11
+ "./api-types": "./dist/api-types.js",
12
+ "./docs": "./dist/docs/index.js",
13
+ "./docs/learning-journey-registry.docblock": "./dist/docs/learning-journey-registry.docblock.js",
14
+ "./example": "./dist/example.js",
15
+ "./learning-journey-registry.feature": "./dist/learning-journey-registry.feature.js",
16
+ "./presentations": "./dist/presentations/index.js",
17
+ "./progress-store": "./dist/progress-store.js",
18
+ "./tracks": "./dist/tracks.js",
19
+ "./ui": "./dist/ui/index.js",
20
+ "./ui/LearningMiniApp": "./dist/ui/LearningMiniApp.js",
21
+ "./*": "./*"
22
+ },
23
+ "scripts": {
24
+ "publish:pkg": "bun publish --tolerate-republish --ignore-scripts --verbose",
25
+ "publish:pkg:canary": "bun publish:pkg --tag canary",
26
+ "build": "bun build:types && bun build:bundle",
27
+ "build:bundle": "tsdown",
28
+ "build:types": "tsc --noEmit",
29
+ "dev": "bun build:bundle --watch",
30
+ "clean": "rimraf dist .turbo",
31
+ "lint": "bun lint:fix",
32
+ "test": "bun test",
33
+ "lint:fix": "eslint src --fix",
34
+ "lint:check": "eslint src"
35
+ },
36
+ "dependencies": {
37
+ "@contractspec/lib.schema": "1.44.0",
38
+ "@contractspec/lib.contracts": "1.44.0",
39
+ "@contractspec/example.learning-journey-ambient-coach": "1.44.0",
40
+ "@contractspec/example.learning-journey-duo-drills": "1.44.0",
41
+ "@contractspec/example.learning-journey-crm-onboarding": "1.44.0",
42
+ "@contractspec/example.learning-journey-platform-tour": "1.44.0",
43
+ "@contractspec/example.learning-journey-quest-challenges": "1.44.0",
44
+ "@contractspec/example.learning-journey-studio-onboarding": "1.44.0",
45
+ "@contractspec/example.learning-journey-ui-shared": "1.44.0",
46
+ "@contractspec/example.learning-journey-ui-gamified": "1.44.0",
47
+ "@contractspec/example.learning-journey-ui-onboarding": "1.44.0",
48
+ "@contractspec/example.learning-journey-ui-coaching": "1.44.0",
49
+ "@contractspec/module.learning-journey": "1.44.0",
50
+ "react": "19.2.3"
51
+ },
52
+ "devDependencies": {
53
+ "@contractspec/tool.tsdown": "1.44.0",
54
+ "@contractspec/tool.typescript": "1.44.0",
55
+ "@types/react": "^19.1.6",
56
+ "tsdown": "^0.18.3",
57
+ "typescript": "^5.9.3"
58
+ },
59
+ "peerDependencies": {
60
+ "react": "^19.2.3"
61
+ },
62
+ "module": "./dist/index.js",
63
+ "publishConfig": {
64
+ "exports": {
65
+ ".": "./dist/index.js",
66
+ "./api": "./dist/api.js",
67
+ "./api-types": "./dist/api-types.js",
68
+ "./docs": "./dist/docs/index.js",
69
+ "./docs/learning-journey-registry.docblock": "./dist/docs/learning-journey-registry.docblock.js",
70
+ "./example": "./dist/example.js",
71
+ "./learning-journey-registry.feature": "./dist/learning-journey-registry.feature.js",
72
+ "./presentations": "./dist/presentations/index.js",
73
+ "./progress-store": "./dist/progress-store.js",
74
+ "./tracks": "./dist/tracks.js",
75
+ "./ui": "./dist/ui/index.js",
76
+ "./ui/LearningMiniApp": "./dist/ui/LearningMiniApp.js",
77
+ "./*": "./*"
78
+ },
79
+ "registry": "https://registry.npmjs.org/",
80
+ "access": "public"
81
+ },
82
+ "license": "MIT",
83
+ "repository": {
84
+ "type": "git",
85
+ "url": "https://github.com/lssm-tech/contractspec.git",
86
+ "directory": "packages/examples/learning-journey-registry"
87
+ },
88
+ "homepage": "https://contractspec.io"
89
+ }
@@ -0,0 +1,43 @@
1
+ import type { LearningJourneyTrackSpec } from '@contractspec/module.learning-journey/track-spec';
2
+
3
+ export interface LearningEvent {
4
+ name: string;
5
+ version?: number;
6
+ sourceModule?: string;
7
+ payload?: Record<string, unknown>;
8
+ occurredAt?: Date;
9
+ learnerId: string;
10
+ trackId?: string;
11
+ }
12
+
13
+ export type StepStatus = 'PENDING' | 'COMPLETED';
14
+
15
+ export interface StepProgress {
16
+ id: string;
17
+ status: StepStatus;
18
+ xpEarned: number;
19
+ completedAt?: Date;
20
+ triggeringEvent?: string;
21
+ eventPayload?: Record<string, unknown>;
22
+ occurrences?: number;
23
+ counterStartedAt?: Date;
24
+ availableAt?: Date;
25
+ dueAt?: Date;
26
+ masteryCount?: number;
27
+ }
28
+
29
+ export interface TrackProgress {
30
+ learnerId: string;
31
+ trackId: string;
32
+ progress: number;
33
+ isCompleted: boolean;
34
+ xpEarned: number;
35
+ startedAt?: Date;
36
+ completedAt?: Date;
37
+ lastActivityAt?: Date;
38
+ steps: StepProgress[];
39
+ }
40
+
41
+ export type TrackResolver = (
42
+ trackId: string
43
+ ) => LearningJourneyTrackSpec | undefined;
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+
3
+ import { recordEvent, getProgress, listTracks } from './api';
4
+ import { learningJourneyTracks } from './tracks';
5
+
6
+ const learnerId = 'learner-1';
7
+
8
+ describe('learning journey registry api', () => {
9
+ it('completes studio track and applies streak bonus', () => {
10
+ const track = learningJourneyTracks.find(
11
+ (t) => t.id === 'studio_getting_started'
12
+ );
13
+ expect(track).toBeDefined();
14
+
15
+ const events = [
16
+ { name: 'studio.template.instantiated' },
17
+ { name: 'spec.changed', payload: { scope: 'sandbox' } },
18
+ { name: 'regeneration.completed' },
19
+ { name: 'module.navigated', payload: { moduleId: 'canvas' } },
20
+ { name: 'studio.evolution.applied' },
21
+ ];
22
+
23
+ events.forEach((evt) =>
24
+ recordEvent({
25
+ ...evt,
26
+ learnerId,
27
+ occurredAt: new Date(),
28
+ })
29
+ );
30
+
31
+ const progress = getProgress('studio_getting_started', learnerId);
32
+ expect(progress).toBeDefined();
33
+ if (!progress) return;
34
+
35
+ expect(progress.isCompleted).toBeTrue();
36
+ expect(progress.progress).toBe(100);
37
+ // base xp: track totalXp (110) + completion bonus (25) + streak bonus (25)
38
+ expect(progress.xpEarned).toBeGreaterThanOrEqual(160);
39
+ });
40
+
41
+ it('lists tracks with empty progress for new learner', () => {
42
+ const result = listTracks('new-learner');
43
+ expect(result.tracks.length).toBeGreaterThan(0);
44
+ expect(result.progress.length).toBe(0);
45
+ });
46
+ });
package/src/api.ts ADDED
@@ -0,0 +1,301 @@
1
+ import { learningJourneyTracks } from './tracks';
2
+ import type {
3
+ LearningJourneyTrackSpec,
4
+ StepAvailabilitySpec,
5
+ StepCompletionConditionSpec,
6
+ } from '@contractspec/module.learning-journey/track-spec';
7
+ import type { LearningEvent, StepProgress, TrackProgress } from './api-types';
8
+ import {
9
+ getLearnerTracks,
10
+ getTrackResolver,
11
+ initProgress,
12
+ } from './progress-store';
13
+
14
+ const getTrack = getTrackResolver(learningJourneyTracks);
15
+
16
+ const matchesFilter = (
17
+ filter: Record<string, unknown> | undefined,
18
+ payload: Record<string, unknown> | undefined
19
+ ): boolean => {
20
+ if (!filter) return true;
21
+ if (!payload) return false;
22
+ return Object.entries(filter).every(([key, value]) => payload[key] === value);
23
+ };
24
+
25
+ const matchesBaseEvent = (
26
+ condition: {
27
+ eventName: string;
28
+ eventVersion?: number;
29
+ sourceModule?: string;
30
+ payloadFilter?: Record<string, unknown>;
31
+ },
32
+ event: LearningEvent
33
+ ): boolean => {
34
+ if (condition.eventName !== event.name) return false;
35
+ if (condition.eventVersion !== undefined && event.version !== undefined) {
36
+ if (condition.eventVersion !== event.version) return false;
37
+ }
38
+ if (
39
+ condition.sourceModule &&
40
+ event.sourceModule &&
41
+ condition.sourceModule !== event.sourceModule
42
+ ) {
43
+ return false;
44
+ }
45
+ return matchesFilter(
46
+ condition.payloadFilter,
47
+ event.payload as Record<string, unknown> | undefined
48
+ );
49
+ };
50
+
51
+ const matchesCondition = (
52
+ condition: StepCompletionConditionSpec,
53
+ event: LearningEvent,
54
+ step: StepProgress,
55
+ trackStartedAt: Date | undefined
56
+ ): {
57
+ matched: boolean;
58
+ occurrences?: number;
59
+ masteryCount?: number;
60
+ } => {
61
+ if (condition.kind === 'count') {
62
+ if (!matchesBaseEvent(condition, event)) return { matched: false };
63
+ const occurrences = (step.occurrences ?? 0) + 1;
64
+ const within =
65
+ condition.withinHours === undefined ||
66
+ Boolean(
67
+ trackStartedAt &&
68
+ event.occurredAt &&
69
+ (event.occurredAt.getTime() - trackStartedAt.getTime()) /
70
+ (1000 * 60 * 60) <=
71
+ condition.withinHours
72
+ );
73
+ return { matched: within && occurrences >= condition.atLeast, occurrences };
74
+ }
75
+
76
+ if (condition.kind === 'time_window') {
77
+ if (!matchesBaseEvent(condition, event)) return { matched: false };
78
+ if (
79
+ condition.withinHoursOfStart !== undefined &&
80
+ trackStartedAt &&
81
+ event.occurredAt
82
+ ) {
83
+ const hoursSinceStart =
84
+ (event.occurredAt.getTime() - trackStartedAt.getTime()) /
85
+ (1000 * 60 * 60);
86
+ if (hoursSinceStart > condition.withinHoursOfStart) {
87
+ return { matched: false };
88
+ }
89
+ }
90
+ return { matched: true };
91
+ }
92
+
93
+ if (condition.kind === 'srs_mastery') {
94
+ if (event.name !== condition.eventName) return { matched: false };
95
+ const payload = event.payload as Record<string, unknown> | undefined;
96
+ if (!matchesFilter(condition.payloadFilter, payload)) {
97
+ return { matched: false };
98
+ }
99
+ const skillKey = condition.skillIdField ?? 'skillId';
100
+ const masteryKey = condition.masteryField ?? 'mastery';
101
+ const skillId = payload?.[skillKey];
102
+ const masteryValue = payload?.[masteryKey];
103
+ if (skillId === undefined || masteryValue === undefined) {
104
+ return { matched: false };
105
+ }
106
+ if (typeof masteryValue !== 'number') return { matched: false };
107
+ if (masteryValue < condition.minimumMastery) return { matched: false };
108
+ const masteryCount = (step.masteryCount ?? 0) + 1;
109
+ const required = condition.requiredCount ?? 1;
110
+ return { matched: masteryCount >= required, masteryCount };
111
+ }
112
+
113
+ return { matched: matchesBaseEvent(condition, event) };
114
+ };
115
+
116
+ const getAvailability = (
117
+ availability: StepAvailabilitySpec | undefined,
118
+ startedAt: Date | undefined
119
+ ): { availableAt?: Date; dueAt?: Date } => {
120
+ if (!availability || !startedAt) return {};
121
+
122
+ const baseTime = startedAt.getTime();
123
+ let unlockTime = baseTime;
124
+
125
+ if (availability.unlockOnDay !== undefined) {
126
+ unlockTime =
127
+ baseTime + (availability.unlockOnDay - 1) * 24 * 60 * 60 * 1000;
128
+ }
129
+
130
+ if (availability.unlockAfterHours !== undefined) {
131
+ unlockTime = baseTime + availability.unlockAfterHours * 60 * 60 * 1000;
132
+ }
133
+
134
+ const availableAt = new Date(unlockTime);
135
+ const dueAt =
136
+ availability.dueWithinHours !== undefined
137
+ ? new Date(
138
+ availableAt.getTime() + availability.dueWithinHours * 60 * 60 * 1000
139
+ )
140
+ : undefined;
141
+
142
+ return { availableAt, dueAt };
143
+ };
144
+
145
+ const computeProgressPercent = (steps: StepProgress[]): number => {
146
+ const total = steps.length || 1;
147
+ const done = steps.filter((s) => s.status === 'COMPLETED').length;
148
+ return Math.round((done / total) * 100);
149
+ };
150
+
151
+ const applyTrackCompletionBonuses = (
152
+ track: LearningJourneyTrackSpec,
153
+ progress: TrackProgress
154
+ ) => {
155
+ if (progress.isCompleted) return progress;
156
+
157
+ const completedAt = new Date();
158
+ const startedAt = progress.startedAt ?? completedAt;
159
+ const hoursElapsed =
160
+ (completedAt.getTime() - startedAt.getTime()) / (1000 * 60 * 60);
161
+
162
+ let xpEarned = progress.xpEarned;
163
+ const { completionRewards, streakRule } = track;
164
+
165
+ if (completionRewards?.xpBonus) {
166
+ xpEarned += completionRewards.xpBonus;
167
+ }
168
+
169
+ if (
170
+ streakRule?.hoursWindow !== undefined &&
171
+ hoursElapsed <= streakRule.hoursWindow &&
172
+ streakRule.bonusXp
173
+ ) {
174
+ xpEarned += streakRule.bonusXp;
175
+ }
176
+
177
+ return {
178
+ ...progress,
179
+ xpEarned,
180
+ isCompleted: true,
181
+ completedAt,
182
+ lastActivityAt: completedAt,
183
+ };
184
+ };
185
+
186
+ export const listTracks = (learnerId?: string) => {
187
+ const progressMap = learnerId ? getLearnerTracks(learnerId) : undefined;
188
+ const progress =
189
+ learnerId && progressMap
190
+ ? Array.from(progressMap.values())
191
+ : ([] as TrackProgress[]);
192
+
193
+ return {
194
+ tracks: learningJourneyTracks,
195
+ progress,
196
+ };
197
+ };
198
+
199
+ export const getProgress = (trackId: string, learnerId: string) => {
200
+ const track = getTrack(trackId);
201
+ if (!track) return undefined;
202
+
203
+ const map = getLearnerTracks(learnerId);
204
+ const existing = map.get(trackId) ?? initProgress(learnerId, track);
205
+ map.set(trackId, existing);
206
+ return existing;
207
+ };
208
+
209
+ export const recordEvent = (event: LearningEvent) => {
210
+ const targets =
211
+ event.trackId !== undefined
212
+ ? learningJourneyTracks.filter((t) => t.id === event.trackId)
213
+ : learningJourneyTracks;
214
+
215
+ const updated: TrackProgress[] = [];
216
+ const eventTime = event.occurredAt ?? new Date();
217
+
218
+ for (const track of targets) {
219
+ const map = getLearnerTracks(event.learnerId);
220
+ const current = map.get(track.id) ?? initProgress(event.learnerId, track);
221
+ const startedAt = current.startedAt ?? eventTime;
222
+
223
+ let changed = current.startedAt === undefined;
224
+ const steps: StepProgress[] = current.steps.map((step) => {
225
+ if (step.status === 'COMPLETED') return step;
226
+
227
+ const spec = track.steps.find((s) => s.id === step.id);
228
+ if (!spec) return step;
229
+
230
+ const { availableAt, dueAt } = getAvailability(
231
+ spec.availability,
232
+ startedAt
233
+ );
234
+ if (availableAt && eventTime < availableAt) {
235
+ return { ...step, availableAt, dueAt };
236
+ }
237
+ if (dueAt && eventTime > dueAt) {
238
+ // keep pending but note deadlines
239
+ return { ...step, availableAt, dueAt };
240
+ }
241
+
242
+ const result = matchesCondition(spec.completion, event, step, startedAt);
243
+
244
+ if (result.matched) {
245
+ changed = true;
246
+ return {
247
+ ...step,
248
+ status: 'COMPLETED',
249
+ xpEarned: spec.xpReward ?? 0,
250
+ completedAt: eventTime,
251
+ triggeringEvent: event.name,
252
+ eventPayload: event.payload,
253
+ occurrences: result.occurrences ?? step.occurrences,
254
+ masteryCount: result.masteryCount ?? step.masteryCount,
255
+ availableAt,
256
+ dueAt,
257
+ };
258
+ }
259
+
260
+ if (
261
+ result.occurrences !== undefined ||
262
+ result.masteryCount !== undefined
263
+ ) {
264
+ changed = true;
265
+ }
266
+
267
+ return {
268
+ ...step,
269
+ occurrences: result.occurrences ?? step.occurrences,
270
+ masteryCount: result.masteryCount ?? step.masteryCount,
271
+ availableAt,
272
+ dueAt,
273
+ };
274
+ });
275
+
276
+ if (!changed) {
277
+ continue;
278
+ }
279
+
280
+ const xpEarned =
281
+ steps.reduce((sum, s) => sum + s.xpEarned, 0) + (track.totalXp ?? 0);
282
+ let progress: TrackProgress = {
283
+ ...current,
284
+ steps,
285
+ xpEarned,
286
+ startedAt,
287
+ lastActivityAt: eventTime,
288
+ progress: computeProgressPercent(steps),
289
+ };
290
+
291
+ const allDone = steps.every((s) => s.status === 'COMPLETED');
292
+ if (allDone) {
293
+ progress = applyTrackCompletionBonuses(track, progress);
294
+ }
295
+
296
+ map.set(track.id, progress);
297
+ updated.push(progress);
298
+ }
299
+
300
+ return updated;
301
+ };
@@ -0,0 +1 @@
1
+ import './learning-journey-registry.docblock';