@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,576 @@
1
+ import { expect, test } from "bun:test";
2
+ import {
3
+ Qti301DerivedZodTemplates,
4
+ QtiAccessForAllPnpDocumentSchema,
5
+ QtiAsiProfileDocumentSchema,
6
+ QtiAssessmentItemDocumentSchema,
7
+ QtiAssessmentResultDocumentSchema,
8
+ QtiAssessmentSectionDocumentSchema,
9
+ QtiAssessmentStimulusDocumentSchema,
10
+ QtiAssessmentTestDocumentSchema,
11
+ QtiMetadataDocumentSchema,
12
+ QtiOutcomeDeclarationDocumentSchema,
13
+ QtiOutcomeProcessingDocumentSchema,
14
+ QtiResponseProcessingDocumentSchema,
15
+ QtiUsageDataDocumentSchema,
16
+ QtiXmlExtensionNodeSchema,
17
+ } from "../src";
18
+
19
+ test("QtiMetadataDocumentSchema parses a minimal metadata document", () => {
20
+ const parsed = QtiMetadataDocumentSchema.safeParse({
21
+ qtiMetadata: {
22
+ itemTemplate: false,
23
+ interactionType: ["choiceInteraction"],
24
+ scoringMode: ["responseprocessing"],
25
+ },
26
+ });
27
+
28
+ expect(parsed.success).toBe(true);
29
+ });
30
+
31
+ test("QtiAccessForAllPnpDocumentSchema parses a minimal preferences document", () => {
32
+ const parsed = QtiAccessForAllPnpDocumentSchema.safeParse({
33
+ accessForAllPnp: {
34
+ languageOfInterface: [{ xmlLang: "en" }],
35
+ magnification: {
36
+ allContent: { zoomAmount: 1.5 },
37
+ },
38
+ dictionaryOnScreen: true,
39
+ },
40
+ });
41
+
42
+ expect(parsed.success).toBe(true);
43
+ });
44
+
45
+ test("QtiAssessmentResultDocumentSchema parses a minimal result report", () => {
46
+ const parsed = QtiAssessmentResultDocumentSchema.safeParse({
47
+ assessmentResult: {
48
+ context: {
49
+ sessionIdentifiers: [
50
+ {
51
+ sourceId: "https://example.test/delivery",
52
+ identifier: "session-1",
53
+ },
54
+ ],
55
+ },
56
+ itemResults: [
57
+ {
58
+ identifier: "ITEM1",
59
+ datestamp: "2026-05-27T12:00:00Z",
60
+ sessionStatus: "final",
61
+ responseVariables: [
62
+ {
63
+ identifier: "RESPONSE",
64
+ cardinality: "single",
65
+ baseType: "identifier",
66
+ candidateResponse: {
67
+ values: [{ value: "A" }],
68
+ },
69
+ scoreStatus: "scored",
70
+ answeredStatus: "answered",
71
+ },
72
+ ],
73
+ outcomeVariables: [
74
+ {
75
+ identifier: "SCORE",
76
+ cardinality: "single",
77
+ baseType: "float",
78
+ values: [{ value: "1.0" }],
79
+ },
80
+ ],
81
+ },
82
+ ],
83
+ },
84
+ });
85
+
86
+ expect(parsed.success).toBe(true);
87
+ });
88
+
89
+ test("QtiAssessmentResultDocumentSchema rejects record response values without field identifiers", () => {
90
+ const parsed = QtiAssessmentResultDocumentSchema.safeParse({
91
+ assessmentResult: {
92
+ context: {},
93
+ itemResults: [
94
+ {
95
+ identifier: "ITEM1",
96
+ datestamp: "2026-05-27T12:00:00Z",
97
+ sessionStatus: "final",
98
+ responseVariables: [
99
+ {
100
+ identifier: "RESPONSE",
101
+ cardinality: "record",
102
+ candidateResponse: {
103
+ values: [{ value: "A" }],
104
+ },
105
+ },
106
+ ],
107
+ },
108
+ ],
109
+ },
110
+ });
111
+
112
+ expect(parsed.success).toBe(false);
113
+ });
114
+
115
+ test("QtiAssessmentResultDocumentSchema rejects record outcomes with baseType", () => {
116
+ const parsed = QtiAssessmentResultDocumentSchema.safeParse({
117
+ assessmentResult: {
118
+ context: {},
119
+ testResult: {
120
+ identifier: "TEST1",
121
+ datestamp: "2026-05-27T12:00:00Z",
122
+ outcomeVariables: [
123
+ {
124
+ identifier: "SCORE_BREAKDOWN",
125
+ cardinality: "record",
126
+ baseType: "float",
127
+ values: [{ value: "1.0", fieldIdentifier: "PART_A" }],
128
+ },
129
+ ],
130
+ },
131
+ },
132
+ });
133
+
134
+ expect(parsed.success).toBe(false);
135
+ });
136
+
137
+ test("QtiUsageDataDocumentSchema parses ordinary and categorized statistics", () => {
138
+ const parsed = QtiUsageDataDocumentSchema.safeParse({
139
+ usageData: {
140
+ glossary: "https://example.test/glossary",
141
+ statistics: [
142
+ {
143
+ kind: "ordinaryStatistic",
144
+ name: "P_VALUE",
145
+ context: "https://example.test/context/FORM_A",
146
+ caseCount: 12345,
147
+ stdError: 0.01,
148
+ lastUpdated: "2026-05-28",
149
+ targetObjects: [
150
+ {
151
+ identifier: "ITEM-1",
152
+ objectType: "item",
153
+ },
154
+ ],
155
+ value: {
156
+ value: "0.65",
157
+ },
158
+ },
159
+ {
160
+ kind: "categorizedStatistic",
161
+ name: "SCORE_CONVERSION",
162
+ context: "https://example.test/context/FORM_A",
163
+ targetObjects: [
164
+ {
165
+ identifier: "TEST-1",
166
+ objectType: "test",
167
+ },
168
+ ],
169
+ mapping: {
170
+ lowerBound: 0,
171
+ upperBound: 2,
172
+ mapEntries: [
173
+ { mapKey: "0", mappedValue: 100 },
174
+ { mapKey: "1", mappedValue: 200 },
175
+ { mapKey: "2", mappedValue: 300 },
176
+ ],
177
+ },
178
+ },
179
+ ],
180
+ },
181
+ });
182
+
183
+ expect(parsed.success).toBe(true);
184
+ });
185
+
186
+ test("QtiUsageDataDocumentSchema rejects invalid objectType and mapping bounds", () => {
187
+ const parsed = QtiUsageDataDocumentSchema.safeParse({
188
+ usageData: {
189
+ statistics: [
190
+ {
191
+ kind: "categorizedStatistic",
192
+ name: "SCORE_CONVERSION",
193
+ context: "https://example.test/context/FORM_A",
194
+ targetObjects: [
195
+ {
196
+ identifier: "TEST-1",
197
+ objectType: "testPart",
198
+ },
199
+ ],
200
+ mapping: {
201
+ lowerBound: 10,
202
+ upperBound: 1,
203
+ mapEntries: [{ mapKey: "A", mappedValue: 1 }],
204
+ },
205
+ },
206
+ ],
207
+ },
208
+ });
209
+
210
+ expect(parsed.success).toBe(false);
211
+ });
212
+
213
+ test("QtiResponseProcessingDocumentSchema parses typed processing operators", () => {
214
+ const parsed = QtiResponseProcessingDocumentSchema.safeParse({
215
+ responseProcessing: {
216
+ rules: [
217
+ {
218
+ kind: "setOutcomeValue",
219
+ identifier: "SCORE",
220
+ expression: {
221
+ kind: "integerDivide",
222
+ children: [
223
+ { kind: "baseValue", baseType: "integer", value: "6" },
224
+ { kind: "baseValue", baseType: "integer", value: "2" },
225
+ ],
226
+ },
227
+ },
228
+ {
229
+ kind: "setOutcomeValue",
230
+ identifier: "MATCH",
231
+ expression: {
232
+ kind: "substring",
233
+ caseSensitive: false,
234
+ children: [
235
+ { kind: "baseValue", baseType: "string", value: "alpha" },
236
+ { kind: "baseValue", baseType: "string", value: "alphabet" },
237
+ ],
238
+ },
239
+ },
240
+ {
241
+ kind: "setOutcomeValue",
242
+ identifier: "ROUNDED",
243
+ expression: {
244
+ kind: "equalRounded",
245
+ figures: 2,
246
+ children: [
247
+ { kind: "baseValue", baseType: "float", value: "1.23" },
248
+ { kind: "baseValue", baseType: "float", value: "1.2301" },
249
+ ],
250
+ },
251
+ },
252
+ ],
253
+ },
254
+ });
255
+
256
+ expect(parsed.success).toBe(true);
257
+ });
258
+
259
+ test("QtiOutcomeProcessingDocumentSchema parses typed aggregate operators", () => {
260
+ const parsed = QtiOutcomeProcessingDocumentSchema.safeParse({
261
+ outcomeProcessing: {
262
+ rules: [
263
+ {
264
+ kind: "setOutcomeValue",
265
+ identifier: "MEAN_SCORE",
266
+ expression: {
267
+ kind: "statsOperator",
268
+ name: "mean",
269
+ children: [
270
+ {
271
+ kind: "testVariables",
272
+ variableIdentifier: "SCORE",
273
+ baseType: "float",
274
+ includeCategory: ["scorable"],
275
+ },
276
+ ],
277
+ },
278
+ },
279
+ {
280
+ kind: "setOutcomeValue",
281
+ identifier: "NUM_CORRECT",
282
+ expression: {
283
+ kind: "numberCorrect",
284
+ sectionIdentifier: "SECTION1",
285
+ },
286
+ },
287
+ {
288
+ kind: "setOutcomeValue",
289
+ identifier: "MAX_SCORE",
290
+ expression: {
291
+ kind: "outcomeMaximum",
292
+ outcomeIdentifier: "SCORE",
293
+ includeCategory: ["scorable"],
294
+ },
295
+ },
296
+ ],
297
+ },
298
+ });
299
+
300
+ expect(parsed.success).toBe(true);
301
+ });
302
+
303
+ test("QtiResponseProcessingDocumentSchema rejects invalid operator parameters", () => {
304
+ const parsed = QtiResponseProcessingDocumentSchema.safeParse({
305
+ responseProcessing: {
306
+ rules: [
307
+ {
308
+ kind: "setOutcomeValue",
309
+ identifier: "INDEXED",
310
+ expression: {
311
+ kind: "index",
312
+ n: 0,
313
+ children: [{ kind: "variable", identifier: "RESPONSE" }],
314
+ },
315
+ },
316
+ {
317
+ kind: "setOutcomeValue",
318
+ identifier: "ROUNDED",
319
+ expression: {
320
+ kind: "roundTo",
321
+ roundingMode: "decimalPlaces",
322
+ figures: 0,
323
+ children: [{ kind: "baseValue", baseType: "float", value: "1.234" }],
324
+ },
325
+ },
326
+ ],
327
+ },
328
+ });
329
+
330
+ expect(parsed.success).toBe(false);
331
+ });
332
+
333
+ test("QtiOutcomeProcessingDocumentSchema rejects invalid operator arity and ranges", () => {
334
+ const parsed = QtiOutcomeProcessingDocumentSchema.safeParse({
335
+ outcomeProcessing: {
336
+ rules: [
337
+ {
338
+ kind: "setOutcomeValue",
339
+ identifier: "ANGLE",
340
+ expression: {
341
+ kind: "mathOperator",
342
+ name: "atan2",
343
+ children: [{ kind: "baseValue", baseType: "float", value: "1.0" }],
344
+ },
345
+ },
346
+ {
347
+ kind: "setOutcomeValue",
348
+ identifier: "WINDOW",
349
+ expression: {
350
+ kind: "anyN",
351
+ min: 2,
352
+ max: 1,
353
+ children: [{ kind: "baseValue", baseType: "boolean", value: "true" }],
354
+ },
355
+ },
356
+ ],
357
+ },
358
+ });
359
+
360
+ expect(parsed.success).toBe(false);
361
+ });
362
+
363
+ test("QtiAssessmentSectionDocumentSchema parses a minimal standalone section", () => {
364
+ const parsed = QtiAssessmentSectionDocumentSchema.safeParse({
365
+ assessmentSection: {
366
+ identifier: "SECTION1",
367
+ title: "Standalone Section",
368
+ visible: true,
369
+ children: [
370
+ {
371
+ identifier: "ITEMREF1",
372
+ href: "items/item1.xml",
373
+ },
374
+ ],
375
+ },
376
+ });
377
+
378
+ expect(parsed.success).toBe(true);
379
+ });
380
+
381
+ test("QtiAssessmentStimulusDocumentSchema parses a minimal stimulus", () => {
382
+ const parsed = QtiAssessmentStimulusDocumentSchema.safeParse({
383
+ assessmentStimulus: {
384
+ identifier: "STIM1",
385
+ title: "Shared Stimulus",
386
+ stimulusBody: {
387
+ content: ["Read the passage before answering the questions."],
388
+ },
389
+ },
390
+ });
391
+
392
+ expect(parsed.success).toBe(true);
393
+ });
394
+
395
+ test("QtiOutcomeDeclarationDocumentSchema parses a minimal outcome declaration", () => {
396
+ const parsed = QtiOutcomeDeclarationDocumentSchema.safeParse({
397
+ outcomeDeclaration: {
398
+ identifier: "SCORE",
399
+ cardinality: "single",
400
+ baseType: "float",
401
+ },
402
+ });
403
+
404
+ expect(parsed.success).toBe(true);
405
+ });
406
+
407
+ test("QtiAsiProfileDocumentSchema accepts resource-specific QTI documents", () => {
408
+ const parsed = QtiAsiProfileDocumentSchema.safeParse({
409
+ responseProcessing: {
410
+ rules: [
411
+ {
412
+ kind: "setOutcomeValue",
413
+ identifier: "SCORE",
414
+ expression: {
415
+ kind: "baseValue",
416
+ baseType: "float",
417
+ value: "1.0",
418
+ },
419
+ },
420
+ ],
421
+ },
422
+ });
423
+
424
+ expect(parsed.success).toBe(true);
425
+ });
426
+
427
+ test("QtiAssessmentItemDocumentSchema parses a minimal choice item", () => {
428
+ const parsed = QtiAssessmentItemDocumentSchema.safeParse({
429
+ assessmentItem: {
430
+ identifier: "ITEM1",
431
+ title: "Sample Item",
432
+ timeDependent: false,
433
+ responseDeclarations: [
434
+ {
435
+ identifier: "RESPONSE",
436
+ cardinality: "single",
437
+ baseType: "identifier",
438
+ },
439
+ ],
440
+ outcomeDeclarations: [
441
+ {
442
+ identifier: "SCORE",
443
+ cardinality: "single",
444
+ baseType: "float",
445
+ },
446
+ ],
447
+ itemBody: {
448
+ content: [
449
+ {
450
+ kind: "choiceInteraction",
451
+ responseIdentifier: "RESPONSE",
452
+ simpleChoices: [
453
+ {
454
+ kind: "simpleChoice",
455
+ identifier: "A",
456
+ content: ["Choice A"],
457
+ },
458
+ {
459
+ kind: "simpleChoice",
460
+ identifier: "B",
461
+ content: ["Choice B"],
462
+ },
463
+ ],
464
+ },
465
+ ],
466
+ },
467
+ responseProcessing: {
468
+ rules: [
469
+ {
470
+ kind: "setOutcomeValue",
471
+ identifier: "SCORE",
472
+ expression: {
473
+ kind: "baseValue",
474
+ baseType: "float",
475
+ value: "1.0",
476
+ },
477
+ },
478
+ ],
479
+ },
480
+ },
481
+ });
482
+
483
+ expect(parsed.success).toBe(true);
484
+ });
485
+
486
+ test("QtiAssessmentItemDocumentSchema rejects incompatible response declarations", () => {
487
+ const parsed = QtiAssessmentItemDocumentSchema.safeParse({
488
+ assessmentItem: {
489
+ identifier: "ITEM2",
490
+ title: "Invalid Item",
491
+ timeDependent: false,
492
+ responseDeclarations: [
493
+ {
494
+ identifier: "RESPONSE",
495
+ cardinality: "single",
496
+ baseType: "string",
497
+ },
498
+ ],
499
+ outcomeDeclarations: [
500
+ {
501
+ identifier: "SCORE",
502
+ cardinality: "single",
503
+ baseType: "float",
504
+ },
505
+ ],
506
+ itemBody: {
507
+ content: [
508
+ {
509
+ kind: "choiceInteraction",
510
+ responseIdentifier: "RESPONSE",
511
+ simpleChoices: [
512
+ { kind: "simpleChoice", identifier: "A", content: ["A"] },
513
+ { kind: "simpleChoice", identifier: "B", content: ["B"] },
514
+ ],
515
+ },
516
+ ],
517
+ },
518
+ },
519
+ });
520
+
521
+ expect(parsed.success).toBe(false);
522
+ });
523
+
524
+ test("QtiAssessmentTestDocumentSchema parses a minimal test document", () => {
525
+ const parsed = QtiAssessmentTestDocumentSchema.safeParse({
526
+ assessmentTest: {
527
+ identifier: "TEST1",
528
+ title: "Sample Test",
529
+ outcomeDeclarations: [
530
+ {
531
+ identifier: "SCORE",
532
+ cardinality: "single",
533
+ baseType: "float",
534
+ },
535
+ ],
536
+ testParts: [
537
+ {
538
+ identifier: "PART1",
539
+ navigationMode: "linear",
540
+ submissionMode: "individual",
541
+ children: [
542
+ {
543
+ identifier: "SECTION1",
544
+ title: "Section 1",
545
+ visible: true,
546
+ children: [
547
+ {
548
+ identifier: "ITEMREF1",
549
+ href: "items/item1.xml",
550
+ },
551
+ ],
552
+ },
553
+ ],
554
+ },
555
+ ],
556
+ },
557
+ });
558
+
559
+ expect(parsed.success).toBe(true);
560
+ });
561
+
562
+ test("Qti301DerivedZodTemplates exposes expected document templates", () => {
563
+ expect(Qti301DerivedZodTemplates.qtiAssessmentItemDocument).toBe(QtiAssessmentItemDocumentSchema);
564
+ expect(Qti301DerivedZodTemplates.qtiAssessmentResultDocument).toBe(QtiAssessmentResultDocumentSchema);
565
+ expect(Qti301DerivedZodTemplates.qtiUsageDataDocument).toBe(QtiUsageDataDocumentSchema);
566
+ });
567
+
568
+ test("QTI barrel exports prefixed XML extension node helpers", () => {
569
+ const parsed = QtiXmlExtensionNodeSchema.safeParse({
570
+ namespace: "urn:test",
571
+ name: "custom",
572
+ value: "x",
573
+ });
574
+
575
+ expect(parsed.success).toBe(true);
576
+ });
@@ -0,0 +1,101 @@
1
+ import { expect, test } from "bun:test";
2
+ import { readFileSync } from "node:fs";
3
+ import { resolve } from "node:path";
4
+ import { AdapterProfileSchema } from "../src/adapter";
5
+ import { RunnerConfigSchema } from "../src/config";
6
+ import { RequirementTraceSchema, RunMetadataSchema, RunnerSummarySchema } from "../src/summary";
7
+
8
+ function readJson(path: string): unknown {
9
+ return JSON.parse(readFileSync(path, "utf8"));
10
+ }
11
+
12
+ test("example configs parse with RunnerConfigSchema", () => {
13
+ const root = resolve(import.meta.dir, "..", "..", "..");
14
+ const configs = [
15
+ resolve(root, "examples/configs/lrs.basic.json"),
16
+ resolve(root, "examples/configs/cmi5.oracle.json"),
17
+ resolve(root, "examples/configs/lti13.core-launch.json"),
18
+ ];
19
+
20
+ for (const file of configs) {
21
+ const parsed = RunnerConfigSchema.safeParse(readJson(file));
22
+ expect(parsed.success).toBe(true);
23
+ }
24
+ });
25
+
26
+ test("summary sample shape validates", () => {
27
+ const parsed = RunnerSummarySchema.safeParse({
28
+ contractVersion: "1.0.0",
29
+ runner: {
30
+ suite: "cmi5",
31
+ version: "0.1.0",
32
+ profileVersion: "1.0.0",
33
+ target: "all",
34
+ },
35
+ startedAt: "2026-05-24T12:00:00.000Z",
36
+ finishedAt: "2026-05-24T12:00:01.000Z",
37
+ durationMs: 1000,
38
+ result: {
39
+ status: "passed",
40
+ passed: 0,
41
+ failed: 0,
42
+ skipped: 0,
43
+ },
44
+ artifacts: {
45
+ summaryFile: "summary.json",
46
+ junitFile: "junit.xml",
47
+ requirementTraceFile: "requirement-trace.json",
48
+ runMetadataFile: "run-metadata.json",
49
+ },
50
+ });
51
+
52
+ expect(parsed.success).toBe(true);
53
+ });
54
+
55
+ test("requirement trace sample shape validates", () => {
56
+ const parsed = RequirementTraceSchema.safeParse({
57
+ contractVersion: "1.0.0",
58
+ runId: "run-001",
59
+ requirements: {
60
+ "9.3.1.0-2": {
61
+ status: "passed",
62
+ evidence: ["launch statement present"],
63
+ },
64
+ },
65
+ });
66
+
67
+ expect(parsed.success).toBe(true);
68
+ });
69
+
70
+ test("run metadata sample shape validates", () => {
71
+ const parsed = RunMetadataSchema.safeParse({
72
+ runId: "run-001",
73
+ startedAt: "2026-05-24T12:00:00.000Z",
74
+ finishedAt: "2026-05-24T12:00:01.000Z",
75
+ image: {
76
+ reference: "ghcr.io/conform-ed/cmi5-runner:v0.1.0-rc.1",
77
+ digest: "sha256:abc123",
78
+ source: "https://github.com/conform-ed/conform-ed",
79
+ },
80
+ standards: {
81
+ suiteSourceRevision: "catapult-lts-806c0b",
82
+ requirementsRevision: "requirements-v1",
83
+ profileVersion: "1.0.0",
84
+ },
85
+ runner: {
86
+ version: "0.1.0",
87
+ revision: "abcdef123456",
88
+ },
89
+ });
90
+
91
+ expect(parsed.success).toBe(true);
92
+ });
93
+
94
+ test("adapter profile sample shape validates", () => {
95
+ const root = resolve(import.meta.dir, "..", "..", "..");
96
+ const parsed = AdapterProfileSchema.safeParse(
97
+ readJson(resolve(root, "examples/profiles/cmi5.http.adapter-profile.json")),
98
+ );
99
+
100
+ expect(parsed.success).toBe(true);
101
+ });