@conform-ed/contracts 0.0.3

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 (187) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-format.log +6 -0
  3. package/.turbo/turbo-lint.log +4 -0
  4. package/.turbo/turbo-test.log +196 -0
  5. package/.turbo/turbo-typecheck.log +2 -0
  6. package/README.md +52 -0
  7. package/caliper-v1_2-zod-templates.md +123 -0
  8. package/case-v1_1-zod-templates.md +290 -0
  9. package/cat-v1_0-zod-templates.md +73 -0
  10. package/cc-v1_3-zod-templates.md +531 -0
  11. package/cc-v1_4-zod-templates.md +247 -0
  12. package/clr-v2_0-zod-templates.md +117 -0
  13. package/cmi5-v1_0-zod-templates.md +9 -0
  14. package/lti-zod-templates.md +11 -0
  15. package/oneroster-v1_2-zod-templates.md +76 -0
  16. package/open-badges-v3_0-zod-templates.md +66 -0
  17. package/package.json +42 -0
  18. package/qti-v2_1-zod-templates.md +45 -0
  19. package/qti-v2_2-zod-templates.md +32 -0
  20. package/qti-v3_0_1-zod-templates.md +421 -0
  21. package/src/adapter.ts +83 -0
  22. package/src/caliper/v1_2/caliper_v1p2_bootcamp_schema.ts +781 -0
  23. package/src/caliper/v1_2/index.ts +37 -0
  24. package/src/caliper/v1_2/shared.ts +122 -0
  25. package/src/caliper/v1_2/textual_requirements.ts +219 -0
  26. package/src/case/v1_1/case_v1p1_cfassociation_jsonschema1.ts +1 -0
  27. package/src/case/v1_1/case_v1p1_cfassociationgrouping_jsonschema1.ts +1 -0
  28. package/src/case/v1_1/case_v1p1_cfassociationset_jsonschema1.ts +1 -0
  29. package/src/case/v1_1/case_v1p1_cfconceptset_jsonschema1.ts +1 -0
  30. package/src/case/v1_1/case_v1p1_cfdocument_jsonschema1.ts +1 -0
  31. package/src/case/v1_1/case_v1p1_cfdocumentset_jsonschema1.ts +1 -0
  32. package/src/case/v1_1/case_v1p1_cfitem_jsonschema1.ts +1 -0
  33. package/src/case/v1_1/case_v1p1_cfitemtypeset_jsonschema1.ts +1 -0
  34. package/src/case/v1_1/case_v1p1_cflicense_jsonschema1.ts +1 -0
  35. package/src/case/v1_1/case_v1p1_cfpackage_jsonschema1.ts +1 -0
  36. package/src/case/v1_1/case_v1p1_cfrubric_jsonschema1.ts +1 -0
  37. package/src/case/v1_1/case_v1p1_cfsubjectset_jsonschema1.ts +1 -0
  38. package/src/case/v1_1/case_v1p1_imsx_statusinfo_jsonschema1.ts +1 -0
  39. package/src/case/v1_1/case_v1p1_openapi3_restbinding_schema.ts +113 -0
  40. package/src/case/v1_1/index.ts +95 -0
  41. package/src/case/v1_1/shared.ts +384 -0
  42. package/src/cat/v1_0/cat_v1p0_restbinding_operations_schema.ts +75 -0
  43. package/src/cat/v1_0/index.ts +124 -0
  44. package/src/cat/v1_0/shared.ts +345 -0
  45. package/src/clr/v2_0/clr_v2p0_achievementcredential_schema.ts +16 -0
  46. package/src/clr/v2_0/clr_v2p0_clrcredential_schema.ts +9 -0
  47. package/src/clr/v2_0/clr_v2p0_endorsementcredential_schema.ts +11 -0
  48. package/src/clr/v2_0/clr_v2p0_getclrcredentialsresponse_schema.ts +1 -0
  49. package/src/clr/v2_0/clr_v2p0_imsx_statusinfo_schema.ts +8 -0
  50. package/src/clr/v2_0/clr_v2p0_profile_schema.ts +9 -0
  51. package/src/clr/v2_0/index.ts +22 -0
  52. package/src/clr/v2_0/shared.ts +206 -0
  53. package/src/cmi5/index.ts +3 -0
  54. package/src/cmi5/v1_0/index.ts +134 -0
  55. package/src/common-cartridge/v1_3/ccv1p3_imsccauth_v1p3.ts +26 -0
  56. package/src/common-cartridge/v1_3/ccv1p3_imscp_v1p2_v1p0.ts +352 -0
  57. package/src/common-cartridge/v1_3/ccv1p3_imscsmd_v1p0.ts +35 -0
  58. package/src/common-cartridge/v1_3/ccv1p3_imsdt_v1p3.ts +33 -0
  59. package/src/common-cartridge/v1_3/ccv1p3_imswl_v1p3.ts +23 -0
  60. package/src/common-cartridge/v1_3/ccv1p3_lomccltilink_v1p0.ts +14 -0
  61. package/src/common-cartridge/v1_3/ccv1p3_lommanifest_v1p0.ts +14 -0
  62. package/src/common-cartridge/v1_3/ccv1p3_lomresource_v1p0.ts +14 -0
  63. package/src/common-cartridge/v1_3/ccv1p3_qtiasiv1p2p1_v1p0.ts +964 -0
  64. package/src/common-cartridge/v1_3/index.ts +41 -0
  65. package/src/common-cartridge/v1_3/lom-internal.ts +396 -0
  66. package/src/common-cartridge/v1_3/shared.ts +68 -0
  67. package/src/common-cartridge/v1_4/core/ccv1p4_imscp_v1p2_v1p0.ts +360 -0
  68. package/src/common-cartridge/v1_4/core/ccv1p4_lommanifest_v1p0.ts +14 -0
  69. package/src/common-cartridge/v1_4/core/ccv1p4_lomresource_v1p0.ts +14 -0
  70. package/src/common-cartridge/v1_4/extension/cc_extresource_assignmentv1p0_v1p0.ts +61 -0
  71. package/src/common-cartridge/v1_4/extension/ccv1p4_cpextensionv1p2_v1p0.ts +20 -0
  72. package/src/common-cartridge/v1_4/extension/ims_openvideov1p0_v1p0.ts +325 -0
  73. package/src/common-cartridge/v1_4/index.ts +104 -0
  74. package/src/common-cartridge/v1_4/k12/ccv1p4_imscp_v1p2_v1p0.ts +19 -0
  75. package/src/common-cartridge/v1_4/k12/ccv1p4_imscp_v1p2_v1p0_thin.ts +17 -0
  76. package/src/common-cartridge/v1_4/k12/ccv1p4_lommanifest_v1p0.ts +14 -0
  77. package/src/common-cartridge/v1_4/k12/ccv1p4_lomresource_v1p0.ts +14 -0
  78. package/src/common-cartridge/v1_4/lom-internal.ts +476 -0
  79. package/src/common-cartridge/v1_4/shared/ccv1p4_imsccauth_v1p4.ts +26 -0
  80. package/src/common-cartridge/v1_4/shared/ccv1p4_imscsmd_v1p1.ts +36 -0
  81. package/src/common-cartridge/v1_4/shared/ccv1p4_imsdt_v1p4.ts +33 -0
  82. package/src/common-cartridge/v1_4/shared/ccv1p4_imslticc_v1p4.ts +45 -0
  83. package/src/common-cartridge/v1_4/shared/ccv1p4_imswl_v1p4.ts +23 -0
  84. package/src/common-cartridge/v1_4/shared/ccv1p4_lomccltilink_v1p0.ts +14 -0
  85. package/src/common-cartridge/v1_4/shared/ccv1p4_qtiasiv1p2p1_v1p0.ts +964 -0
  86. package/src/common-cartridge/v1_4/shared/imsbasiclti_v1p0p1.ts +24 -0
  87. package/src/common-cartridge/v1_4/shared/imslticm_v1p0.ts +23 -0
  88. package/src/common-cartridge/v1_4/shared/imslticp_v1p0.ts +57 -0
  89. package/src/common-cartridge/v1_4/shared/lineitem_v1p0.ts +35 -0
  90. package/src/common-cartridge/v1_4/shared/resourcea11ymetadata-20210915.ts +110 -0
  91. package/src/common-cartridge/v1_4/shared.ts +68 -0
  92. package/src/common-cartridge/v1_4/thin/ccv1p4_imscp_v1p2_v1p0.ts +243 -0
  93. package/src/common-cartridge/v1_4/thin/ccv1p4_lommanifest_v1p0.ts +14 -0
  94. package/src/common-cartridge/v1_4/thin/ccv1p4_lomresource_v1p0.ts +14 -0
  95. package/src/common-cartridge/v1_4/vdex/imsmd_loose_v1p3p2.ts +13 -0
  96. package/src/common-cartridge/v1_4/vdex/imsvdex_v1p0.ts +124 -0
  97. package/src/config.ts +121 -0
  98. package/src/index.ts +32 -0
  99. package/src/lti/ags/v2_0/index.ts +97 -0
  100. package/src/lti/deep-linking/v2_0/index.ts +100 -0
  101. package/src/lti/index.ts +26 -0
  102. package/src/lti/nrps/v2_0/index.ts +69 -0
  103. package/src/lti/proctoring/v1_0/index.ts +131 -0
  104. package/src/lti/shared.ts +66 -0
  105. package/src/lti/v1_3/index.ts +84 -0
  106. package/src/oneroster/v1_2/index.ts +156 -0
  107. package/src/oneroster/v1_2/or_v1p2_csv_binding_schema.ts +427 -0
  108. package/src/oneroster/v1_2/or_v1p2_gradebook_restbinding_schema.ts +120 -0
  109. package/src/oneroster/v1_2/or_v1p2_gradebook_service_schema.ts +243 -0
  110. package/src/oneroster/v1_2/or_v1p2_resource_restbinding_schema.ts +24 -0
  111. package/src/oneroster/v1_2/or_v1p2_resource_service_schema.ts +47 -0
  112. package/src/oneroster/v1_2/or_v1p2_rostering_restbinding_schema.ts +84 -0
  113. package/src/oneroster/v1_2/or_v1p2_rostering_service_schema.ts +288 -0
  114. package/src/oneroster/v1_2/shared.ts +90 -0
  115. package/src/open-badges/v3_0/index.ts +20 -0
  116. package/src/open-badges/v3_0/ob_v3p0_achievementcredential_schema.ts +17 -0
  117. package/src/open-badges/v3_0/ob_v3p0_endorsementcredential_schema.ts +11 -0
  118. package/src/open-badges/v3_0/ob_v3p0_getopenbadgecredentialsresponse_schema.ts +1 -0
  119. package/src/open-badges/v3_0/ob_v3p0_imsx_statusinfo_schema.ts +8 -0
  120. package/src/open-badges/v3_0/ob_v3p0_profile_schema.ts +9 -0
  121. package/src/open-badges/v3_0/shared.ts +458 -0
  122. package/src/qti/v2-internal.ts +1683 -0
  123. package/src/qti/v2_1/apipv1p0_qtiextv2p1_v1p0.ts +3 -0
  124. package/src/qti/v2_1/imsqti_metadata_v2p1.ts +3 -0
  125. package/src/qti/v2_1/imsqti_result_v2p1.ts +3 -0
  126. package/src/qti/v2_1/imsqti_usagedata_v2p1.ts +3 -0
  127. package/src/qti/v2_1/imsqti_v2p1p2.ts +71 -0
  128. package/src/qti/v2_1/index.ts +53 -0
  129. package/src/qti/v2_1/qtiv2p1_imscpv1p2_v1p0.ts +3 -0
  130. package/src/qti/v2_1/schemas.ts +15 -0
  131. package/src/qti/v2_1/shared.ts +3 -0
  132. package/src/qti/v2_2/apipv1p0_qtiextv2p2_v1p0p1.ts +3 -0
  133. package/src/qti/v2_2/imsqti_metadata_v2p2.ts +3 -0
  134. package/src/qti/v2_2/imsqti_result_v2p2.ts +3 -0
  135. package/src/qti/v2_2/imsqti_usagedata_v2p2.ts +3 -0
  136. package/src/qti/v2_2/imsqti_v2p2p4.ts +76 -0
  137. package/src/qti/v2_2/imsqtiv2p2p4_html5_v1p0.ts +8 -0
  138. package/src/qti/v2_2/index.ts +59 -0
  139. package/src/qti/v2_2/qtiv2p2_csm_v2p2.ts +4 -0
  140. package/src/qti/v2_2/qtiv2p2_imscpv1p2_v1p0.ts +3 -0
  141. package/src/qti/v2_2/schemas.ts +19 -0
  142. package/src/qti/v2_2/shared.ts +3 -0
  143. package/src/qti/v3_0_1/assessment-internal.ts +1509 -0
  144. package/src/qti/v3_0_1/imsqti_asiv3p0p1_v1p0.ts +57 -0
  145. package/src/qti/v3_0_1/imsqti_itemv3p0p1_v1p0.ts +60 -0
  146. package/src/qti/v3_0_1/imsqti_metadatav3p0_v1p0.ts +73 -0
  147. package/src/qti/v3_0_1/imsqti_outcomev3p0p1_v1p0.ts +11 -0
  148. package/src/qti/v3_0_1/imsqti_responseprocessingv3p0p1_v1p0.ts +17 -0
  149. package/src/qti/v3_0_1/imsqti_resultv3p0_v1p0.ts +222 -0
  150. package/src/qti/v3_0_1/imsqti_sectionv3p0p1_v1p0.ts +15 -0
  151. package/src/qti/v3_0_1/imsqti_stimulusv3p0p1_v1p0.ts +15 -0
  152. package/src/qti/v3_0_1/imsqti_testv3p0p1_v1p0.ts +49 -0
  153. package/src/qti/v3_0_1/imsqti_usagedatav3p0_v1p0.ts +77 -0
  154. package/src/qti/v3_0_1/imsqtiv3p0_afa3p0pnp_v1p0.ts +269 -0
  155. package/src/qti/v3_0_1/index.ts +50 -0
  156. package/src/qti/v3_0_1/processing-internal.ts +667 -0
  157. package/src/qti/v3_0_1/shared.ts +146 -0
  158. package/src/qti/v3_0_1/variables-internal.ts +274 -0
  159. package/src/shared/imsx-status.ts +49 -0
  160. package/src/summary.ts +70 -0
  161. package/src/vc-data-model/v2_0/index.ts +27 -0
  162. package/src/vc-data-model/v2_0/shared.ts +206 -0
  163. package/src/xapi/index.ts +12 -0
  164. package/src/xapi/shared.ts +444 -0
  165. package/src/xapi/v1_0_3/index.ts +169 -0
  166. package/src/xapi/v2_0/index.ts +256 -0
  167. package/test/caliper-v1_2.test.ts +270 -0
  168. package/test/case-v1_1.test.ts +147 -0
  169. package/test/cat-v1_0.test.ts +338 -0
  170. package/test/cc-v1_4.test.ts +239 -0
  171. package/test/clr-v2_0.test.ts +225 -0
  172. package/test/cmi5-v1_0.test.ts +196 -0
  173. package/test/contracts.test.ts +125 -0
  174. package/test/fixtures/cmi5/extended-cmi5.xml +72 -0
  175. package/test/fixtures/cmi5/simple-cmi5.xml +26 -0
  176. package/test/lti.test.ts +146 -0
  177. package/test/oneroster-v1_2.test.ts +234 -0
  178. package/test/open-badges-v3_0.test.ts +144 -0
  179. package/test/qti-v2_1.test.ts +146 -0
  180. package/test/qti-v2_2.test.ts +154 -0
  181. package/test/qti-v3_0_1.test.ts +576 -0
  182. package/test/schema-examples.test.ts +101 -0
  183. package/test/vc-data-model-v2_0.test.ts +63 -0
  184. package/test/xapi.test.ts +384 -0
  185. package/tsconfig.json +7 -0
  186. package/vc-data-model-v2_0-zod-templates.md +43 -0
  187. package/xapi-zod-templates.md +95 -0
@@ -0,0 +1,239 @@
1
+ import { expect, test } from "bun:test";
2
+
3
+ import { CommonCartridgeV1_4 } from "../src";
4
+
5
+ test("CommonCartridgeV1_4 parses a minimal core manifest", () => {
6
+ const parsed = CommonCartridgeV1_4.CommonCartridgeManifestProfileDocumentSchema.safeParse({
7
+ manifest: {
8
+ identifier: "MANIFEST1",
9
+ metadata: {
10
+ schema: "IMS Common Cartridge",
11
+ schemaversion: "1.4.0",
12
+ lom: {},
13
+ },
14
+ organizations: {
15
+ organization: {
16
+ identifier: "ORG1",
17
+ structure: "rooted-hierarchy",
18
+ item: {
19
+ identifier: "ROOT",
20
+ },
21
+ },
22
+ },
23
+ resources: {
24
+ resource: [
25
+ {
26
+ identifier: "RES1",
27
+ type: "webcontent",
28
+ href: "index.html",
29
+ file: [{ href: "index.html" }],
30
+ },
31
+ ],
32
+ },
33
+ },
34
+ });
35
+
36
+ expect(parsed.success).toBe(true);
37
+ });
38
+
39
+ test("CommonCartridgeV1_4 rejects question-bank references from organization items", () => {
40
+ const parsed = CommonCartridgeV1_4.CommonCartridgeManifestProfileDocumentSchema.safeParse({
41
+ manifest: {
42
+ identifier: "MANIFEST1",
43
+ metadata: {
44
+ schema: "IMS Common Cartridge",
45
+ schemaversion: "1.4.0",
46
+ lom: {},
47
+ },
48
+ organizations: {
49
+ organization: {
50
+ identifier: "ORG1",
51
+ structure: "rooted-hierarchy",
52
+ item: {
53
+ identifier: "ROOT",
54
+ item: [
55
+ {
56
+ identifier: "ITEM1",
57
+ identifierref: "QB1",
58
+ title: "Linked question bank",
59
+ },
60
+ ],
61
+ },
62
+ },
63
+ },
64
+ resources: {
65
+ resource: [
66
+ {
67
+ identifier: "QB1",
68
+ type: "imsqti_xmlv1p2/imscc_xmlv1p4/question-bank",
69
+ file: [{ href: "bank.xml" }],
70
+ },
71
+ ],
72
+ },
73
+ },
74
+ });
75
+
76
+ expect(parsed.success).toBe(false);
77
+ });
78
+
79
+ test("CommonCartridgeV1_4 parses a minimal thin manifest with embedded webLink XML", () => {
80
+ const parsed = CommonCartridgeV1_4.ThinCommonCartridgeManifestProfileDocumentSchema.safeParse({
81
+ manifest: {
82
+ identifier: "THIN1",
83
+ metadata: {
84
+ schema: "IMS Thin Common Cartridge",
85
+ schemaversion: "1.4.0",
86
+ lom: {},
87
+ },
88
+ organizations: {
89
+ organization: {
90
+ identifier: "ORG1",
91
+ structure: "rooted-hierarchy",
92
+ item: {
93
+ identifier: "ROOT",
94
+ },
95
+ },
96
+ },
97
+ resources: {
98
+ resource: [
99
+ {
100
+ identifier: "LINK1",
101
+ type: "imswl_xmlv1p4",
102
+ webLink: {
103
+ title: "External reading",
104
+ url: {
105
+ href: "https://example.test/resource",
106
+ },
107
+ },
108
+ },
109
+ ],
110
+ },
111
+ },
112
+ });
113
+
114
+ expect(parsed.success).toBe(true);
115
+ });
116
+
117
+ test("CommonCartridgeV1_4 thin profile rejects mismatched embedded resource XML", () => {
118
+ const parsed = CommonCartridgeV1_4.ThinCommonCartridgeManifestProfileDocumentSchema.safeParse({
119
+ manifest: {
120
+ identifier: "THIN1",
121
+ metadata: {
122
+ schema: "IMS Thin Common Cartridge",
123
+ schemaversion: "1.4.0",
124
+ lom: {},
125
+ },
126
+ organizations: {
127
+ organization: {
128
+ identifier: "ORG1",
129
+ structure: "rooted-hierarchy",
130
+ item: {
131
+ identifier: "ROOT",
132
+ },
133
+ },
134
+ },
135
+ resources: {
136
+ resource: [
137
+ {
138
+ identifier: "LINK1",
139
+ type: "imswl_xmlv1p4",
140
+ cartridge_basiclti_link: {
141
+ title: "Tool",
142
+ vendor: {
143
+ code: "tool-vendor",
144
+ name: { value: "Tool Vendor" },
145
+ },
146
+ },
147
+ },
148
+ ],
149
+ },
150
+ },
151
+ });
152
+
153
+ expect(parsed.success).toBe(false);
154
+ });
155
+
156
+ test("CommonCartridgeV1_4 K-12 LOM resource profile enforces required educational metadata", () => {
157
+ const parsed = CommonCartridgeV1_4.K12LomResourceDocumentSchema.safeParse({
158
+ lom: {
159
+ general: {
160
+ title: {
161
+ string: [{ value: "Algebra lesson" }],
162
+ },
163
+ },
164
+ educational: [
165
+ {
166
+ intendedEndUserRole: [{ value: "learner" }],
167
+ },
168
+ ],
169
+ },
170
+ });
171
+
172
+ expect(parsed.success).toBe(false);
173
+ });
174
+
175
+ test("CommonCartridgeV1_4 parses assignment, line item, accessibility, and open video documents", () => {
176
+ const assignment = CommonCartridgeV1_4.AssignmentDocumentSchema.safeParse({
177
+ assignment: {
178
+ identifier: "ASSIGN1",
179
+ title: "Essay",
180
+ text: {
181
+ value: "Write a short essay.",
182
+ texttype: "text/plain",
183
+ },
184
+ },
185
+ });
186
+
187
+ const lineItem = CommonCartridgeV1_4.LineItemDocumentSchema.safeParse({
188
+ lineItem: {
189
+ scoreMaximum: 100,
190
+ label: "Homework 1",
191
+ },
192
+ });
193
+
194
+ const accessibility = CommonCartridgeV1_4.ResourceAccessibilityMetadataDocumentSchema.safeParse({
195
+ ResourceAccessibilityMetadata: {
196
+ accessibilityFeature: ["captions"],
197
+ accessMode: ["visual"],
198
+ },
199
+ });
200
+
201
+ const openVideo = CommonCartridgeV1_4.OpenVideoSessionDocumentSchema.safeParse({
202
+ session: {
203
+ title: "Lecture capture",
204
+ streams: {
205
+ stream: [
206
+ {
207
+ startTime: "PT0S",
208
+ file: {
209
+ url: "https://example.test/video.mp4",
210
+ mimeType: "video/mp4",
211
+ },
212
+ source: "presenter",
213
+ providesAudio: true,
214
+ },
215
+ ],
216
+ },
217
+ },
218
+ });
219
+
220
+ expect(assignment.success).toBe(true);
221
+ expect(lineItem.success).toBe(true);
222
+ expect(accessibility.success).toBe(true);
223
+ expect(openVideo.success).toBe(true);
224
+ });
225
+
226
+ test("CommonCartridgeV1_4 exposes expected derived templates", () => {
227
+ expect(CommonCartridgeV1_4.CommonCartridgeDerivedZodTemplates.commonCartridgeManifestProfileDocument).toBe(
228
+ CommonCartridgeV1_4.CommonCartridgeManifestProfileDocumentSchema,
229
+ );
230
+ expect(CommonCartridgeV1_4.CommonCartridgeDerivedZodTemplates.thinCommonCartridgeManifestProfileDocument).toBe(
231
+ CommonCartridgeV1_4.ThinCommonCartridgeManifestProfileDocumentSchema,
232
+ );
233
+ expect(CommonCartridgeV1_4.CommonCartridgeDerivedZodTemplates.k12CommonCartridgeManifestProfileDocument).toBe(
234
+ CommonCartridgeV1_4.K12CommonCartridgeManifestProfileDocumentSchema,
235
+ );
236
+ expect(CommonCartridgeV1_4.CommonCartridgeDerivedZodTemplates.assignmentDocument).toBe(
237
+ CommonCartridgeV1_4.AssignmentDocumentSchema,
238
+ );
239
+ });
@@ -0,0 +1,225 @@
1
+ import { expect, test } from "bun:test";
2
+
3
+ import { ClrV2_0 } from "../src";
4
+
5
+ test("ClrCredentialSchema parses a realistic CLR 2.0 credential", () => {
6
+ const parsed = ClrV2_0.ClrCredentialSchema.safeParse({
7
+ "@context": [
8
+ "https://www.w3.org/ns/credentials/v2",
9
+ "https://purl.imsglobal.org/spec/clr/v2p0/context-2.0.1.json",
10
+ "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json",
11
+ "https://purl.imsglobal.org/spec/ob/v3p0/extensions.json",
12
+ ],
13
+ id: "http://example.edu/credentials/3732",
14
+ type: ["VerifiableCredential", "ClrCredential"],
15
+ issuer: {
16
+ id: "https://example.edu/issuers/565049",
17
+ type: ["Profile"],
18
+ name: "Example University",
19
+ },
20
+ validFrom: "2010-01-01T00:00:00Z",
21
+ name: "Sample Transcript",
22
+ credentialSubject: {
23
+ id: "did:example:ebfeb1f712ebc6f1c276e12ec21",
24
+ type: ["ClrSubject"],
25
+ verifiableCredential: [
26
+ {
27
+ "@context": [
28
+ "https://www.w3.org/ns/credentials/v2",
29
+ "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json",
30
+ ],
31
+ id: "http://example.com/credentials/3527",
32
+ type: ["VerifiableCredential", "OpenBadgeCredential"],
33
+ issuer: {
34
+ id: "https://example.com/issuers/876543",
35
+ type: ["Profile"],
36
+ name: "Example Corp",
37
+ },
38
+ validFrom: "2010-01-01T00:00:00Z",
39
+ credentialSubject: {
40
+ id: "did:example:ebfeb1f712ebc6f1c276e12ec21",
41
+ achievement: {
42
+ id: "https://example.com/achievements/21st-century-skills/teamwork",
43
+ type: ["Achievement"],
44
+ criteria: {
45
+ narrative: "Team members are nominated for this badge by their peers and recognized after review.",
46
+ },
47
+ description: "This badge recognizes the capacity to collaborate within a group environment.",
48
+ name: "Teamwork",
49
+ },
50
+ },
51
+ proof: [
52
+ {
53
+ type: "DataIntegrityProof",
54
+ created: "2024-03-21T18:13:02Z",
55
+ verificationMethod: "https://example.com/issuers/876543#z6MkvREVgsHx7Ppae68vCoByy73ZD4aMSJiPML2cryVL8JAx",
56
+ cryptosuite: "eddsa-rdfc-2022",
57
+ proofPurpose: "assertionMethod",
58
+ proofValue: "z5rBXds3Efc6Ks8b4gLqBqFZTnFJM9a6ZeMFTbULckDohcpW4zS4dNP33iDvi6Qej4uJ4vzHvR3wRUSw8ykMS1bR1",
59
+ },
60
+ ],
61
+ },
62
+ ],
63
+ achievement: [
64
+ {
65
+ id: "urn:uuid:a7467ef6-56cb-11ec-bf63-0242ac130002",
66
+ type: ["Achievement"],
67
+ creator: {
68
+ id: "https://example.edu/issuers/565049",
69
+ type: ["Profile"],
70
+ },
71
+ name: "Achievement 1",
72
+ criteria: {
73
+ id: "https://example.edu/achievements/a7467ef6-56cb-11ec-bf63-0242ac130002/criteria",
74
+ },
75
+ description: "Achievement 1",
76
+ },
77
+ ],
78
+ association: [
79
+ {
80
+ type: "Association",
81
+ associationType: "isParentOf",
82
+ sourceId: "urn:uuid:a7467ef6-56cb-11ec-bf63-0242ac130002",
83
+ targetId: "urn:uuid:dd887f0a-56cb-11ec-bf63-0242ac130002",
84
+ },
85
+ ],
86
+ },
87
+ credentialSchema: [
88
+ {
89
+ id: "https://purl.imsglobal.org/spec/clr/v2p0/schema/json/clr_v2p0_clrcredential_schema.json",
90
+ type: "1EdTechJsonSchemaValidator2019",
91
+ },
92
+ ],
93
+ proof: [
94
+ {
95
+ type: "DataIntegrityProof",
96
+ created: "2010-01-01T19:23:24Z",
97
+ verificationMethod: "https://example.edu/issuers/565049#z6MkjZRZv3aez3r18pB1RBFJR1kwUVJ5jHt92JmQwXbd5hwi",
98
+ cryptosuite: "eddsa-rdfc-2022",
99
+ proofPurpose: "assertionMethod",
100
+ proofValue: "z3d7QnJK9rH5H8ARTViDA8oygpawXzqZxY6DwdizBo19rmMWDLKDGwHyF4whGm2WZv7PRNmiw9mmGDjTWoVKXCoWj",
101
+ },
102
+ ],
103
+ });
104
+
105
+ expect(parsed.success).toBe(true);
106
+ });
107
+
108
+ test("AchievementCredentialSchema parses a minimal achievement credential", () => {
109
+ const parsed = ClrV2_0.AchievementCredentialSchema.safeParse({
110
+ "@context": ["https://www.w3.org/ns/credentials/v2", "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json"],
111
+ id: "https://example.test/credentials/achievement-1",
112
+ type: ["VerifiableCredential", "AchievementCredential"],
113
+ issuer: {
114
+ id: "https://example.test/issuers/1",
115
+ type: ["Profile"],
116
+ name: "Example Issuer",
117
+ },
118
+ validFrom: "2025-01-01T00:00:00Z",
119
+ credentialSubject: {
120
+ type: ["AchievementSubject"],
121
+ achievement: {
122
+ id: "https://example.test/achievements/1",
123
+ type: ["Achievement"],
124
+ name: "Example Achievement",
125
+ description: "Awarded for completing the example pathway.",
126
+ criteria: {
127
+ narrative: "Complete every required task.",
128
+ },
129
+ },
130
+ },
131
+ });
132
+
133
+ expect(parsed.success).toBe(true);
134
+ });
135
+
136
+ test("ProfileSchema parses a minimal issuer profile", () => {
137
+ const parsed = ClrV2_0.ProfileSchema.safeParse({
138
+ id: "https://example.test/issuers/1",
139
+ type: ["Profile"],
140
+ name: "Example Issuer",
141
+ url: "https://example.test",
142
+ });
143
+
144
+ expect(parsed.success).toBe(true);
145
+ });
146
+
147
+ test("GetClrCredentialsResponseSchema parses credential and JWS collections", () => {
148
+ const parsed = ClrV2_0.GetClrCredentialsResponseSchema.safeParse({
149
+ credential: {
150
+ "@context": [
151
+ "https://www.w3.org/ns/credentials/v2",
152
+ "https://purl.imsglobal.org/spec/clr/v2p0/context-2.0.1.json",
153
+ "https://purl.imsglobal.org/spec/ob/v3p0/context-3.0.3.json",
154
+ ],
155
+ id: "https://example.test/credentials/1",
156
+ type: ["VerifiableCredential", "ClrCredential"],
157
+ issuer: {
158
+ id: "https://example.test/issuers/1",
159
+ type: ["Profile"],
160
+ },
161
+ validFrom: "2025-01-01T00:00:00Z",
162
+ name: "Example CLR",
163
+ credentialSubject: {
164
+ type: ["ClrSubject"],
165
+ verifiableCredential: {
166
+ "@context": ["https://www.w3.org/ns/credentials/v2"],
167
+ type: "VerifiableCredential",
168
+ issuer: "https://example.test/issuers/1",
169
+ validFrom: "2025-01-01T00:00:00Z",
170
+ credentialSubject: {
171
+ id: "did:example:student-1",
172
+ },
173
+ },
174
+ },
175
+ },
176
+ compactJwsString: "header.payload.signature",
177
+ });
178
+
179
+ expect(parsed.success).toBe(true);
180
+ });
181
+
182
+ test("ImsxStatusInfoSchema enforces known status vocabularies", () => {
183
+ const parsed = ClrV2_0.ImsxStatusInfoSchema.safeParse({
184
+ imsx_codeMajor: "success",
185
+ imsx_severity: "status",
186
+ imsx_description: "Request completed successfully",
187
+ imsx_codeMinor: {
188
+ imsx_codeMinorField: {
189
+ imsx_codeMinorFieldName: "clr-service",
190
+ imsx_codeMinorFieldValue: "fullsuccess",
191
+ },
192
+ },
193
+ });
194
+
195
+ expect(parsed.success).toBe(true);
196
+ });
197
+
198
+ test("ClrCredentialSchema rejects credentials without the CLR context set", () => {
199
+ const parsed = ClrV2_0.ClrCredentialSchema.safeParse({
200
+ "@context": ["https://www.w3.org/ns/credentials/v2"],
201
+ id: "https://example.test/credentials/1",
202
+ type: ["VerifiableCredential", "ClrCredential"],
203
+ issuer: "https://example.test/issuers/1",
204
+ validFrom: "2025-01-01T00:00:00Z",
205
+ name: "Example CLR",
206
+ credentialSubject: {
207
+ type: ["ClrSubject"],
208
+ verifiableCredential: {
209
+ "@context": ["https://www.w3.org/ns/credentials/v2"],
210
+ type: "VerifiableCredential",
211
+ issuer: "https://example.test/issuers/1",
212
+ validFrom: "2025-01-01T00:00:00Z",
213
+ credentialSubject: {},
214
+ },
215
+ },
216
+ });
217
+
218
+ expect(parsed.success).toBe(false);
219
+ });
220
+
221
+ test("Clr20DerivedZodTemplates exposes the published entry points", () => {
222
+ expect(ClrV2_0.Clr20DerivedZodTemplates.clrCredential).toBe(ClrV2_0.ClrCredentialSchema);
223
+ expect(ClrV2_0.Clr20DerivedZodTemplates.profile).toBe(ClrV2_0.ProfileSchema);
224
+ expect(ClrV2_0.Clr20DerivedZodTemplates.imsxStatusInfo).toBe(ClrV2_0.ImsxStatusInfoSchema);
225
+ });
@@ -0,0 +1,196 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { expect, test } from "bun:test";
4
+ import { XMLParser } from "fast-xml-parser";
5
+ import { Cmi5V1_0 } from "@conform-ed/contracts";
6
+
7
+ const parser = new XMLParser({
8
+ ignoreAttributes: false,
9
+ attributeNamePrefix: "@_",
10
+ parseTagValue: false,
11
+ parseAttributeValue: false,
12
+ trimValues: false,
13
+ });
14
+
15
+ function asArray<T>(value: T | T[] | undefined): T[] {
16
+ if (Array.isArray(value)) {
17
+ return value;
18
+ }
19
+ return value === undefined ? [] : [value];
20
+ }
21
+
22
+ function normalizeText(value: string | undefined): string {
23
+ return (value ?? "").replace(/\s+/gu, " ").trim();
24
+ }
25
+
26
+ function readString(record: Record<string, unknown>, key: string): string | undefined {
27
+ const value = record[key];
28
+ return typeof value === "string" ? value : undefined;
29
+ }
30
+
31
+ function normalizeTextBlock(block: unknown): { langstrings: Array<{ lang?: string; value: string }> } {
32
+ const record = (block ?? {}) as Record<string, unknown>;
33
+ return {
34
+ langstrings: asArray(record.langstring as Record<string, unknown> | Array<Record<string, unknown>> | undefined).map(
35
+ (langstring) => ({
36
+ lang: typeof langstring["@_lang"] === "string" ? langstring["@_lang"] : undefined,
37
+ value: normalizeText(typeof langstring["#text"] === "string" ? langstring["#text"] : undefined),
38
+ }),
39
+ ),
40
+ };
41
+ }
42
+
43
+ function normalizeObjectives(value: unknown): Array<{ idref: string }> {
44
+ const record = (value ?? {}) as Record<string, unknown>;
45
+ return asArray(record.objective as Array<Record<string, unknown>> | Record<string, unknown> | undefined).map(
46
+ (objective) => ({
47
+ idref: readString(objective, "@_idref") ?? "",
48
+ }),
49
+ );
50
+ }
51
+
52
+ function normalizeKeywordReferences(value: unknown): Array<{ idref: string }> {
53
+ return asArray(value as Array<Record<string, unknown>> | Record<string, unknown> | undefined).map((keyword) => ({
54
+ idref: readString(keyword, "@_idref") ?? "",
55
+ }));
56
+ }
57
+
58
+ function normalizeCourseItem(item: Record<string, unknown>): unknown {
59
+ if (typeof item.url === "string") {
60
+ return {
61
+ id: readString(item, "@_id") ?? "",
62
+ title: normalizeTextBlock(item.title),
63
+ description: normalizeTextBlock(item.description),
64
+ objectives: item.objectives ? normalizeObjectives(item.objectives) : undefined,
65
+ url: item.url,
66
+ launchParameters: typeof item.launchParameters === "string" ? item.launchParameters : undefined,
67
+ entitlementKey: typeof item.entitlementKey === "string" ? item.entitlementKey : undefined,
68
+ moveOn: typeof item["@_moveOn"] === "string" ? item["@_moveOn"] : undefined,
69
+ masteryScore: typeof item["@_masteryScore"] === "string" ? Number(item["@_masteryScore"]) : undefined,
70
+ launchMethod: typeof item["@_launchMethod"] === "string" ? item["@_launchMethod"] : undefined,
71
+ activityType: typeof item["@_activityType"] === "string" ? item["@_activityType"] : undefined,
72
+ keywords: item["kw:keyword"] ? normalizeKeywordReferences(item["kw:keyword"]) : undefined,
73
+ };
74
+ }
75
+
76
+ return {
77
+ id: readString(item, "@_id") ?? "",
78
+ title: normalizeTextBlock(item.title),
79
+ description: normalizeTextBlock(item.description),
80
+ objectives: item.objectives ? normalizeObjectives(item.objectives) : undefined,
81
+ children: [
82
+ ...asArray(item.au as Array<Record<string, unknown>> | Record<string, unknown> | undefined).map((au) =>
83
+ normalizeCourseItem(au),
84
+ ),
85
+ ...asArray(item.block as Array<Record<string, unknown>> | Record<string, unknown> | undefined).map((block) =>
86
+ normalizeCourseItem(block),
87
+ ),
88
+ ],
89
+ };
90
+ }
91
+
92
+ function normalizeKeywordSet(value: unknown): {
93
+ keywords: Array<{
94
+ id: string;
95
+ title: { langstrings: Array<{ lang?: string; value: string }> };
96
+ description?: { langstrings: Array<{ lang?: string; value: string }> };
97
+ }>;
98
+ } {
99
+ const record = (value ?? {}) as Record<string, unknown>;
100
+ return {
101
+ keywords: asArray(record["kw:keyword"] as Array<Record<string, unknown>> | Record<string, unknown> | undefined).map(
102
+ (keyword) => ({
103
+ id: readString(keyword, "@_id") ?? "",
104
+ title: normalizeTextBlock(keyword["kw:title"]),
105
+ description: keyword["kw:description"] ? normalizeTextBlock(keyword["kw:description"]) : undefined,
106
+ }),
107
+ ),
108
+ };
109
+ }
110
+
111
+ function normalizeDocument(xml: string): unknown {
112
+ const parsed = parser.parse(xml) as Record<string, unknown>;
113
+ const root = (parsed.courseStructure ?? {}) as Record<string, unknown>;
114
+ return {
115
+ courseStructure: {
116
+ course: {
117
+ id: readString((root.course as Record<string, unknown>) ?? {}, "@_id") ?? "",
118
+ title: normalizeTextBlock(((root.course as Record<string, unknown>) ?? {}).title),
119
+ description: normalizeTextBlock(((root.course as Record<string, unknown>) ?? {}).description),
120
+ },
121
+ objectives: root.objectives
122
+ ? asArray((root.objectives as Record<string, unknown>).objective).map((objective) => ({
123
+ id: readString(objective as Record<string, unknown>, "@_id") ?? "",
124
+ title: normalizeTextBlock((objective as Record<string, unknown>).title),
125
+ description: normalizeTextBlock((objective as Record<string, unknown>).description),
126
+ }))
127
+ : undefined,
128
+ children: [
129
+ ...asArray(root.au as Array<Record<string, unknown>> | Record<string, unknown> | undefined).map((au) =>
130
+ normalizeCourseItem(au),
131
+ ),
132
+ ...asArray(root.block as Array<Record<string, unknown>> | Record<string, unknown> | undefined).map((block) =>
133
+ normalizeCourseItem(block),
134
+ ),
135
+ ],
136
+ },
137
+ keywords: root["kw:keywords"] ? normalizeKeywordSet(root["kw:keywords"]) : undefined,
138
+ };
139
+ }
140
+
141
+ function readFixture(name: string): string {
142
+ return readFileSync(resolve(import.meta.dir, "fixtures", "cmi5", name), "utf8");
143
+ }
144
+
145
+ test("cmi5 course structure schema parses the simple XML example", () => {
146
+ const parsed = Cmi5V1_0.Schemas.CourseStructureDocument.safeParse(normalizeDocument(readFixture("simple-cmi5.xml")));
147
+ expect(parsed.success).toBe(true);
148
+ });
149
+
150
+ test("cmi5 keyword extension schema parses the extended XML example", () => {
151
+ const document = normalizeDocument(readFixture("extended-cmi5.xml")) as {
152
+ keywords?: { keywords: Array<{ id: string }> };
153
+ };
154
+
155
+ expect(Cmi5V1_0.Schemas.CourseStructureDocument.safeParse(document).success).toBe(true);
156
+ expect(Cmi5V1_0.Schemas.KeywordExtension.safeParse(document.keywords).success).toBe(true);
157
+ });
158
+
159
+ test("cmi5 recursive course structure schema accepts nested blocks and AUs", () => {
160
+ const parsed = Cmi5V1_0.Schemas.CourseStructure.safeParse({
161
+ course: {
162
+ id: "http://course-repository.example.edu/identifiers/courses/02baafcf",
163
+ title: { langstrings: [{ lang: "en-US", value: "Introduction to Geology" }] },
164
+ description: { langstrings: [{ lang: "en-US", value: "Course overview" }] },
165
+ },
166
+ objectives: [
167
+ {
168
+ id: "http://objectives.example.com/identifiers/geology/basics",
169
+ title: { langstrings: [{ lang: "en-US", value: "Geology - Basic knowledge" }] },
170
+ description: { langstrings: [{ lang: "en-US", value: "Knowledge about basic terms" }] },
171
+ },
172
+ ],
173
+ children: [
174
+ {
175
+ id: "http://courses.example.edu/identifiers/courses/d07e186b/blocks/001",
176
+ title: { langstrings: [{ lang: "en-US", value: "Geologic materials" }] },
177
+ description: { langstrings: [{ lang: "en-US", value: "Block description" }] },
178
+ objectives: [{ idref: "http://objectives.example.com/identifiers/geology/basics" }],
179
+ children: [
180
+ {
181
+ id: "http://courses.example.edu/identifiers/courses/d07e186b/blocks/001/aus/64f6",
182
+ title: { langstrings: [{ lang: "en-US", value: "Rock and rock cycle" }] },
183
+ description: { langstrings: [{ lang: "en-US", value: "AU description" }] },
184
+ url: "http://courses.example.edu/identifiers/courses/d07e186b/blocks/001/aus/64f6/launch",
185
+ moveOn: "CompletedOrPassed",
186
+ masteryScore: 1,
187
+ launchMethod: "AnyWindow",
188
+ activityType: "http://adlnet.gov/expapi/activities/lesson",
189
+ },
190
+ ],
191
+ },
192
+ ],
193
+ });
194
+
195
+ expect(parsed.success).toBe(true);
196
+ });