@conform-ed/contracts 0.0.12 → 0.0.14

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 (82) hide show
  1. package/.turbo/turbo-build.log +1 -2
  2. package/.turbo/turbo-format.log +5 -5
  3. package/.turbo/turbo-lint.log +3 -3
  4. package/.turbo/turbo-test.log +228 -223
  5. package/.turbo/turbo-typecheck.log +1 -1
  6. package/package.json +4 -4
  7. package/src/adapter.ts +1 -1
  8. package/src/caliper/v1_2/caliper_v1p2_bootcamp_schema.ts +6 -6
  9. package/src/caliper/v1_2/shared.ts +2 -2
  10. package/src/case/v1_1/case_v1p1_openapi3_restbinding_schema.ts +1 -0
  11. package/src/case/v1_1/index.ts +41 -41
  12. package/src/cat/v1_0/index.ts +39 -39
  13. package/src/cat/v1_0/shared.ts +1 -1
  14. package/src/clr/v2_0/shared.ts +19 -17
  15. package/src/cmi5/v1_0/index.ts +19 -19
  16. package/src/common-cartridge/v1_3/ccv1p3_imscp_v1p2_v1p0.ts +3 -3
  17. package/src/common-cartridge/v1_3/ccv1p3_lomccltilink_v1p0.ts +2 -1
  18. package/src/common-cartridge/v1_3/ccv1p3_lommanifest_v1p0.ts +2 -1
  19. package/src/common-cartridge/v1_3/ccv1p3_lomresource_v1p0.ts +2 -1
  20. package/src/common-cartridge/v1_3/ccv1p3_qtiasiv1p2p1_v1p0.ts +16 -13
  21. package/src/common-cartridge/v1_3/index.ts +3 -3
  22. package/src/common-cartridge/v1_3/lom-internal.ts +19 -19
  23. package/src/common-cartridge/v1_3/shared.ts +1 -1
  24. package/src/common-cartridge/v1_4/core/ccv1p4_imscp_v1p2_v1p0.ts +6 -6
  25. package/src/common-cartridge/v1_4/core/ccv1p4_lommanifest_v1p0.ts +2 -1
  26. package/src/common-cartridge/v1_4/core/ccv1p4_lomresource_v1p0.ts +2 -1
  27. package/src/common-cartridge/v1_4/extension/ccv1p4_cpextensionv1p2_v1p0.ts +1 -0
  28. package/src/common-cartridge/v1_4/index.ts +5 -5
  29. package/src/common-cartridge/v1_4/k12/ccv1p4_lommanifest_v1p0.ts +2 -1
  30. package/src/common-cartridge/v1_4/k12/ccv1p4_lomresource_v1p0.ts +2 -1
  31. package/src/common-cartridge/v1_4/lom-internal.ts +30 -27
  32. package/src/common-cartridge/v1_4/shared/ccv1p4_lomccltilink_v1p0.ts +2 -1
  33. package/src/common-cartridge/v1_4/shared/ccv1p4_qtiasiv1p2p1_v1p0.ts +16 -13
  34. package/src/common-cartridge/v1_4/shared.ts +1 -1
  35. package/src/common-cartridge/v1_4/thin/ccv1p4_imscp_v1p2_v1p0.ts +5 -5
  36. package/src/common-cartridge/v1_4/thin/ccv1p4_lommanifest_v1p0.ts +2 -1
  37. package/src/common-cartridge/v1_4/thin/ccv1p4_lomresource_v1p0.ts +2 -1
  38. package/src/common-cartridge/v1_4/vdex/imsmd_loose_v1p3p2.ts +2 -1
  39. package/src/common-cartridge/v1_4/vdex/imsvdex_v1p0.ts +1 -1
  40. package/src/config.ts +3 -3
  41. package/src/h5p/v1/content.ts +1 -0
  42. package/src/h5p/v1/h5p-json.ts +4 -3
  43. package/src/h5p/v1/index.ts +21 -21
  44. package/src/h5p/v1/library-json.ts +2 -1
  45. package/src/lti/ags/v2_0/index.ts +12 -12
  46. package/src/lti/deep-linking/v2_0/index.ts +10 -10
  47. package/src/lti/index.ts +2 -2
  48. package/src/lti/nrps/v2_0/index.ts +10 -10
  49. package/src/lti/proctoring/v1_0/index.ts +11 -11
  50. package/src/lti/v1_3/index.ts +17 -17
  51. package/src/oneroster/v1_2/index.ts +45 -45
  52. package/src/oneroster/v1_2/or_v1p2_gradebook_restbinding_schema.ts +3 -3
  53. package/src/oneroster/v1_2/or_v1p2_resource_restbinding_schema.ts +1 -1
  54. package/src/oneroster/v1_2/or_v1p2_rostering_restbinding_schema.ts +1 -1
  55. package/src/oneroster/v1_2/or_v1p2_rostering_service_schema.ts +1 -1
  56. package/src/open-badges/v3_0/shared.ts +31 -29
  57. package/src/qti/v2-internal.ts +12 -11
  58. package/src/qti/v3_0_1/assessment-internal.ts +239 -197
  59. package/src/qti/v3_0_1/imsqti_asiv3p0p1_v1p0.ts +4 -4
  60. package/src/qti/v3_0_1/imsqti_itemv3p0p1_v1p0.ts +1 -1
  61. package/src/qti/v3_0_1/imsqti_metadatav3p0_v1p0.ts +1 -1
  62. package/src/qti/v3_0_1/imsqti_responseprocessingv3p0p1_v1p0.ts +1 -1
  63. package/src/qti/v3_0_1/imsqti_resultv3p0_v1p0.ts +4 -3
  64. package/src/qti/v3_0_1/imsqti_sectionv3p0p1_v1p0.ts +1 -1
  65. package/src/qti/v3_0_1/imsqti_stimulusv3p0p1_v1p0.ts +1 -1
  66. package/src/qti/v3_0_1/imsqti_testv3p0p1_v1p0.ts +2 -2
  67. package/src/qti/v3_0_1/imsqti_usagedatav3p0_v1p0.ts +1 -1
  68. package/src/qti/v3_0_1/processing-internal.ts +44 -33
  69. package/src/qti/v3_0_1/shared.ts +2 -2
  70. package/src/vc-data-model/v2_0/shared.ts +4 -4
  71. package/src/xapi/shared.ts +11 -14
  72. package/src/xapi/v1_0_3/index.ts +54 -54
  73. package/src/xapi/v2_0/index.ts +56 -56
  74. package/test/caliper-v1_2.test.ts +1 -1
  75. package/test/cat-v1_0.test.ts +1 -0
  76. package/test/cmi5-v1_0.test.ts +33 -31
  77. package/test/contracts.test.ts +1 -0
  78. package/test/h5p-v1.test.ts +1 -0
  79. package/test/lti.test.ts +1 -0
  80. package/test/qti-v3_0_1.test.ts +109 -0
  81. package/test/schema-examples.test.ts +1 -0
  82. package/test/xapi.test.ts +1 -0
@@ -1,5 +1,14 @@
1
1
  import { z } from "zod";
2
2
 
3
+ import {
4
+ QtiBranchRuleSchema,
5
+ QtiIncludeSchema,
6
+ QtiOutcomeProcessingSchema,
7
+ QtiPreConditionSchema,
8
+ QtiResponseProcessingSchema,
9
+ QtiTemplateDefaultSchema,
10
+ QtiTemplateProcessingSchema,
11
+ } from "./processing-internal";
3
12
  import {
4
13
  QtiDirectionSchema,
5
14
  QtiIdentifierListSchema,
@@ -31,15 +40,6 @@ import {
31
40
  QtiVariableMappingSchema,
32
41
  QtiWeightSchema,
33
42
  } from "./variables-internal";
34
- import {
35
- QtiBranchRuleSchema,
36
- QtiIncludeSchema,
37
- QtiOutcomeProcessingSchema,
38
- QtiPreConditionSchema,
39
- QtiResponseProcessingSchema,
40
- QtiTemplateDefaultSchema,
41
- QtiTemplateProcessingSchema,
42
- } from "./processing-internal";
43
43
 
44
44
  const QtiCommonNodeShape = {
45
45
  id: QtiIdentifierSchema.optional(),
@@ -84,9 +84,9 @@ const interactionKinds = new Set<string>([
84
84
  "endAttemptInteraction",
85
85
  ]);
86
86
 
87
- export const QtiXmlContentNodeSchema: z.ZodTypeAny = z.lazy(() => createXmlNodeSchema(QtiContentFragmentSchema));
87
+ export const QtiXmlContentNodeSchema: z.ZodType = z.lazy(() => createXmlNodeSchema(QtiContentFragmentSchema));
88
88
 
89
- export const QtiPromptSchema: z.ZodTypeAny = z.lazy(() =>
89
+ export const QtiPromptSchema: z.ZodType = z.lazy(() =>
90
90
  strictObject({
91
91
  kind: z.literal("prompt"),
92
92
  ...QtiCommonNodeShape,
@@ -94,7 +94,7 @@ export const QtiPromptSchema: z.ZodTypeAny = z.lazy(() =>
94
94
  }),
95
95
  );
96
96
 
97
- export const QtiLabelElementSchema: z.ZodTypeAny = z.lazy(() =>
97
+ export const QtiLabelElementSchema: z.ZodType = z.lazy(() =>
98
98
  strictObject({
99
99
  kind: z.literal("labelElement"),
100
100
  ...QtiCommonNodeShape,
@@ -133,14 +133,14 @@ export const QtiInteractionModulesSchema = strictObject({
133
133
  modules: z.array(QtiInteractionModuleSchema).min(1),
134
134
  });
135
135
 
136
- export const QtiInteractionMarkupSchema: z.ZodTypeAny = z.lazy(() =>
136
+ export const QtiInteractionMarkupSchema: z.ZodType = z.lazy(() =>
137
137
  strictObject({
138
138
  kind: z.literal("interactionMarkup"),
139
139
  content: z.array(QtiContentFragmentSchema).optional(),
140
140
  }),
141
141
  );
142
142
 
143
- export const QtiFeedbackInlineSchema: z.ZodTypeAny = z.lazy(() =>
143
+ export const QtiFeedbackInlineSchema: z.ZodType = z.lazy(() =>
144
144
  strictObject({
145
145
  kind: z.literal("feedbackInline"),
146
146
  ...QtiCommonNodeShape,
@@ -151,7 +151,7 @@ export const QtiFeedbackInlineSchema: z.ZodTypeAny = z.lazy(() =>
151
151
  }),
152
152
  );
153
153
 
154
- export const QtiFeedbackBlockSchema: z.ZodTypeAny = z.lazy(() =>
154
+ export const QtiFeedbackBlockSchema: z.ZodType = z.lazy(() =>
155
155
  strictObject({
156
156
  kind: z.literal("feedbackBlock"),
157
157
  ...QtiCommonNodeShape,
@@ -164,7 +164,7 @@ export const QtiFeedbackBlockSchema: z.ZodTypeAny = z.lazy(() =>
164
164
  }),
165
165
  );
166
166
 
167
- export const QtiTemplateInlineSchema: z.ZodTypeAny = z.lazy(() =>
167
+ export const QtiTemplateInlineSchema: z.ZodType = z.lazy(() =>
168
168
  strictObject({
169
169
  kind: z.literal("templateInline"),
170
170
  ...QtiCommonNodeShape,
@@ -175,7 +175,7 @@ export const QtiTemplateInlineSchema: z.ZodTypeAny = z.lazy(() =>
175
175
  }),
176
176
  );
177
177
 
178
- export const QtiTemplateBlockFeedbackBlockSchema: z.ZodTypeAny = z.lazy(() =>
178
+ export const QtiTemplateBlockFeedbackBlockSchema: z.ZodType = z.lazy(() =>
179
179
  strictObject({
180
180
  kind: z.literal("templateBlockFeedbackBlock"),
181
181
  ...QtiCommonNodeShape,
@@ -188,7 +188,7 @@ export const QtiTemplateBlockFeedbackBlockSchema: z.ZodTypeAny = z.lazy(() =>
188
188
  }),
189
189
  );
190
190
 
191
- export const QtiTemplateBlockSchema: z.ZodTypeAny = z.lazy(() =>
191
+ export const QtiTemplateBlockSchema: z.ZodType = z.lazy(() =>
192
192
  strictObject({
193
193
  kind: z.literal("templateBlock"),
194
194
  ...QtiCommonNodeShape,
@@ -201,7 +201,7 @@ export const QtiTemplateBlockSchema: z.ZodTypeAny = z.lazy(() =>
201
201
  }),
202
202
  );
203
203
 
204
- export const QtiRubricBlockSchema: z.ZodTypeAny = z.lazy(() =>
204
+ export const QtiRubricBlockSchema: z.ZodType = z.lazy(() =>
205
205
  strictObject({
206
206
  kind: z.literal("rubricBlock"),
207
207
  ...QtiCommonNodeShape,
@@ -213,7 +213,7 @@ export const QtiRubricBlockSchema: z.ZodTypeAny = z.lazy(() =>
213
213
  }),
214
214
  );
215
215
 
216
- export const QtiTestRubricBlockSchema: z.ZodTypeAny = z.lazy(() =>
216
+ export const QtiTestRubricBlockSchema: z.ZodType = z.lazy(() =>
217
217
  strictObject({
218
218
  kind: z.literal("testRubricBlock"),
219
219
  ...QtiCommonNodeShape,
@@ -226,7 +226,7 @@ export const QtiTestRubricBlockSchema: z.ZodTypeAny = z.lazy(() =>
226
226
  }),
227
227
  );
228
228
 
229
- export const QtiModalFeedbackSchema: z.ZodTypeAny = z.lazy(() =>
229
+ export const QtiModalFeedbackSchema: z.ZodType = z.lazy(() =>
230
230
  strictObject({
231
231
  kind: z.literal("modalFeedback"),
232
232
  outcomeIdentifier: QtiIdentifierSchema,
@@ -240,7 +240,7 @@ export const QtiModalFeedbackSchema: z.ZodTypeAny = z.lazy(() =>
240
240
  }),
241
241
  );
242
242
 
243
- export const QtiTestFeedbackSchema: z.ZodTypeAny = z.lazy(() =>
243
+ export const QtiTestFeedbackSchema: z.ZodType = z.lazy(() =>
244
244
  strictObject({
245
245
  kind: z.literal("testFeedback"),
246
246
  access: z.enum(["atEnd", "during"]),
@@ -265,7 +265,7 @@ export const QtiGapSchema = strictObject({
265
265
  required: z.boolean().optional(),
266
266
  });
267
267
 
268
- export const QtiGapTextSchema: z.ZodTypeAny = z.lazy(() =>
268
+ export const QtiGapTextSchema: z.ZodType = z.lazy(() =>
269
269
  strictObject({
270
270
  kind: z.literal("gapText"),
271
271
  ...QtiCommonNodeShape,
@@ -279,7 +279,7 @@ export const QtiGapTextSchema: z.ZodTypeAny = z.lazy(() =>
279
279
  }),
280
280
  );
281
281
 
282
- export const QtiGapImgSchema: z.ZodTypeAny = z.lazy(() =>
282
+ export const QtiGapImgSchema: z.ZodType = z.lazy(() =>
283
283
  strictObject({
284
284
  kind: z.literal("gapImg"),
285
285
  ...QtiCommonNodeShape,
@@ -296,7 +296,7 @@ export const QtiGapImgSchema: z.ZodTypeAny = z.lazy(() =>
296
296
  }),
297
297
  );
298
298
 
299
- export const QtiHotTextSchema: z.ZodTypeAny = z.lazy(() =>
299
+ export const QtiHotTextSchema: z.ZodType = z.lazy(() =>
300
300
  strictObject({
301
301
  kind: z.literal("hotText"),
302
302
  ...QtiCommonNodeShape,
@@ -331,7 +331,7 @@ export const QtiAssociableHotspotSchema = strictObject({
331
331
  hotspotLabel: z.string().optional(),
332
332
  });
333
333
 
334
- export const QtiSimpleChoiceSchema: z.ZodTypeAny = z.lazy(() =>
334
+ export const QtiSimpleChoiceSchema: z.ZodType = z.lazy(() =>
335
335
  strictObject({
336
336
  kind: z.literal("simpleChoice"),
337
337
  ...QtiCommonNodeShape,
@@ -343,7 +343,7 @@ export const QtiSimpleChoiceSchema: z.ZodTypeAny = z.lazy(() =>
343
343
  }),
344
344
  );
345
345
 
346
- export const QtiSimpleAssociableChoiceSchema: z.ZodTypeAny = z.lazy(() =>
346
+ export const QtiSimpleAssociableChoiceSchema: z.ZodType = z.lazy(() =>
347
347
  strictObject({
348
348
  kind: z.literal("simpleAssociableChoice"),
349
349
  ...QtiCommonNodeShape,
@@ -358,7 +358,7 @@ export const QtiSimpleAssociableChoiceSchema: z.ZodTypeAny = z.lazy(() =>
358
358
  }),
359
359
  );
360
360
 
361
- export const QtiInlineChoiceSchema: z.ZodTypeAny = z.lazy(() =>
361
+ export const QtiInlineChoiceSchema: z.ZodType = z.lazy(() =>
362
362
  strictObject({
363
363
  kind: z.literal("inlineChoice"),
364
364
  ...QtiCommonNodeShape,
@@ -372,7 +372,7 @@ export const QtiInlineChoiceSchema: z.ZodTypeAny = z.lazy(() =>
372
372
 
373
373
  const QtiRenderableNodeSchema = z.union([QtiXmlContentNodeSchema, QtiIncludeSchema]);
374
374
 
375
- export const QtiChoiceInteractionSchema: z.ZodTypeAny = z.lazy(() =>
375
+ export const QtiChoiceInteractionSchema: z.ZodType = z.lazy(() =>
376
376
  strictObject({
377
377
  kind: z.literal("choiceInteraction"),
378
378
  ...QtiCommonNodeShape,
@@ -388,7 +388,7 @@ export const QtiChoiceInteractionSchema: z.ZodTypeAny = z.lazy(() =>
388
388
  }),
389
389
  );
390
390
 
391
- export const QtiInlineChoiceInteractionSchema: z.ZodTypeAny = z.lazy(() =>
391
+ export const QtiInlineChoiceInteractionSchema: z.ZodType = z.lazy(() =>
392
392
  strictObject({
393
393
  kind: z.literal("inlineChoiceInteraction"),
394
394
  ...QtiCommonNodeShape,
@@ -416,7 +416,7 @@ export const QtiTextEntryInteractionSchema = strictObject({
416
416
  dataPatternmaskMessage: z.string().optional(),
417
417
  });
418
418
 
419
- export const QtiExtendedTextInteractionSchema: z.ZodTypeAny = z.lazy(() =>
419
+ export const QtiExtendedTextInteractionSchema: z.ZodType = z.lazy(() =>
420
420
  strictObject({
421
421
  kind: z.literal("extendedTextInteraction"),
422
422
  ...QtiCommonNodeShape,
@@ -435,7 +435,7 @@ export const QtiExtendedTextInteractionSchema: z.ZodTypeAny = z.lazy(() =>
435
435
  }),
436
436
  );
437
437
 
438
- export const QtiSimpleMatchSetSchema: z.ZodTypeAny = z.lazy(() =>
438
+ export const QtiSimpleMatchSetSchema: z.ZodType = z.lazy(() =>
439
439
  strictObject({
440
440
  kind: z.literal("simpleMatchSet"),
441
441
  id: QtiIdentifierSchema.optional(),
@@ -444,7 +444,7 @@ export const QtiSimpleMatchSetSchema: z.ZodTypeAny = z.lazy(() =>
444
444
  }),
445
445
  );
446
446
 
447
- export const QtiMatchInteractionSchema: z.ZodTypeAny = z.lazy(() =>
447
+ export const QtiMatchInteractionSchema: z.ZodType = z.lazy(() =>
448
448
  strictObject({
449
449
  kind: z.literal("matchInteraction"),
450
450
  ...QtiCommonNodeShape,
@@ -462,7 +462,7 @@ export const QtiMatchInteractionSchema: z.ZodTypeAny = z.lazy(() =>
462
462
 
463
463
  export const QtiGapChoiceSchema = z.union([QtiGapTextSchema, QtiGapImgSchema]);
464
464
 
465
- export const QtiGapMatchInteractionSchema: z.ZodTypeAny = z.lazy(() =>
465
+ export const QtiGapMatchInteractionSchema: z.ZodType = z.lazy(() =>
466
466
  strictObject({
467
467
  kind: z.literal("gapMatchInteraction"),
468
468
  ...QtiCommonNodeShape,
@@ -479,7 +479,7 @@ export const QtiGapMatchInteractionSchema: z.ZodTypeAny = z.lazy(() =>
479
479
  }),
480
480
  );
481
481
 
482
- export const QtiMediaInteractionSchema: z.ZodTypeAny = z.lazy(() =>
482
+ export const QtiMediaInteractionSchema: z.ZodType = z.lazy(() =>
483
483
  strictObject({
484
484
  kind: z.literal("mediaInteraction"),
485
485
  ...QtiCommonNodeShape,
@@ -494,7 +494,7 @@ export const QtiMediaInteractionSchema: z.ZodTypeAny = z.lazy(() =>
494
494
  }),
495
495
  );
496
496
 
497
- export const QtiUploadInteractionSchema: z.ZodTypeAny = z.lazy(() =>
497
+ export const QtiUploadInteractionSchema: z.ZodType = z.lazy(() =>
498
498
  strictObject({
499
499
  kind: z.literal("uploadInteraction"),
500
500
  ...QtiCommonNodeShape,
@@ -504,7 +504,7 @@ export const QtiUploadInteractionSchema: z.ZodTypeAny = z.lazy(() =>
504
504
  }),
505
505
  );
506
506
 
507
- export const QtiOrderInteractionSchema: z.ZodTypeAny = z.lazy(() =>
507
+ export const QtiOrderInteractionSchema: z.ZodType = z.lazy(() =>
508
508
  strictObject({
509
509
  kind: z.literal("orderInteraction"),
510
510
  ...QtiCommonNodeShape,
@@ -521,7 +521,7 @@ export const QtiOrderInteractionSchema: z.ZodTypeAny = z.lazy(() =>
521
521
  }),
522
522
  );
523
523
 
524
- export const QtiHotTextInteractionSchema: z.ZodTypeAny = z.lazy(() =>
524
+ export const QtiHotTextInteractionSchema: z.ZodType = z.lazy(() =>
525
525
  strictObject({
526
526
  kind: z.literal("hotTextInteraction"),
527
527
  ...QtiCommonNodeShape,
@@ -535,7 +535,7 @@ export const QtiHotTextInteractionSchema: z.ZodTypeAny = z.lazy(() =>
535
535
  }),
536
536
  );
537
537
 
538
- export const QtiHotspotInteractionSchema: z.ZodTypeAny = z.lazy(() =>
538
+ export const QtiHotspotInteractionSchema: z.ZodType = z.lazy(() =>
539
539
  strictObject({
540
540
  kind: z.literal("hotspotInteraction"),
541
541
  ...QtiCommonNodeShape,
@@ -550,7 +550,7 @@ export const QtiHotspotInteractionSchema: z.ZodTypeAny = z.lazy(() =>
550
550
  }),
551
551
  );
552
552
 
553
- export const QtiAssociateInteractionSchema: z.ZodTypeAny = z.lazy(() =>
553
+ export const QtiAssociateInteractionSchema: z.ZodType = z.lazy(() =>
554
554
  strictObject({
555
555
  kind: z.literal("associateInteraction"),
556
556
  ...QtiCommonNodeShape,
@@ -563,7 +563,7 @@ export const QtiAssociateInteractionSchema: z.ZodTypeAny = z.lazy(() =>
563
563
  }),
564
564
  );
565
565
 
566
- export const QtiGraphicAssociateInteractionSchema: z.ZodTypeAny = z.lazy(() =>
566
+ export const QtiGraphicAssociateInteractionSchema: z.ZodType = z.lazy(() =>
567
567
  strictObject({
568
568
  kind: z.literal("graphicAssociateInteraction"),
569
569
  ...QtiCommonNodeShape,
@@ -576,7 +576,7 @@ export const QtiGraphicAssociateInteractionSchema: z.ZodTypeAny = z.lazy(() =>
576
576
  }),
577
577
  );
578
578
 
579
- export const QtiGraphicGapMatchInteractionSchema: z.ZodTypeAny = z.lazy(() =>
579
+ export const QtiGraphicGapMatchInteractionSchema: z.ZodType = z.lazy(() =>
580
580
  strictObject({
581
581
  kind: z.literal("graphicGapMatchInteraction"),
582
582
  ...QtiCommonNodeShape,
@@ -593,7 +593,7 @@ export const QtiGraphicGapMatchInteractionSchema: z.ZodTypeAny = z.lazy(() =>
593
593
  }),
594
594
  );
595
595
 
596
- export const QtiGraphicOrderInteractionSchema: z.ZodTypeAny = z.lazy(() =>
596
+ export const QtiGraphicOrderInteractionSchema: z.ZodType = z.lazy(() =>
597
597
  strictObject({
598
598
  kind: z.literal("graphicOrderInteraction"),
599
599
  ...QtiCommonNodeShape,
@@ -606,7 +606,7 @@ export const QtiGraphicOrderInteractionSchema: z.ZodTypeAny = z.lazy(() =>
606
606
  }),
607
607
  );
608
608
 
609
- export const QtiSelectPointInteractionSchema: z.ZodTypeAny = z.lazy(() =>
609
+ export const QtiSelectPointInteractionSchema: z.ZodType = z.lazy(() =>
610
610
  strictObject({
611
611
  kind: z.literal("selectPointInteraction"),
612
612
  ...QtiCommonNodeShape,
@@ -618,7 +618,7 @@ export const QtiSelectPointInteractionSchema: z.ZodTypeAny = z.lazy(() =>
618
618
  }),
619
619
  );
620
620
 
621
- export const QtiSliderInteractionSchema: z.ZodTypeAny = z.lazy(() =>
621
+ export const QtiSliderInteractionSchema: z.ZodType = z.lazy(() =>
622
622
  strictObject({
623
623
  kind: z.literal("sliderInteraction"),
624
624
  ...QtiCommonNodeShape,
@@ -633,7 +633,7 @@ export const QtiSliderInteractionSchema: z.ZodTypeAny = z.lazy(() =>
633
633
  }),
634
634
  );
635
635
 
636
- export const QtiPositionObjectInteractionSchema: z.ZodTypeAny = z.lazy(() =>
636
+ export const QtiPositionObjectInteractionSchema: z.ZodType = z.lazy(() =>
637
637
  strictObject({
638
638
  kind: z.literal("positionObjectInteraction"),
639
639
  ...QtiCommonNodeShape,
@@ -645,7 +645,7 @@ export const QtiPositionObjectInteractionSchema: z.ZodTypeAny = z.lazy(() =>
645
645
  }),
646
646
  );
647
647
 
648
- export const QtiPortableCustomInteractionSchema: z.ZodTypeAny = z.lazy(() =>
648
+ export const QtiPortableCustomInteractionSchema: z.ZodType = z.lazy(() =>
649
649
  strictObject({
650
650
  kind: z.literal("portableCustomInteraction"),
651
651
  ...QtiCommonNodeShape,
@@ -659,10 +659,12 @@ export const QtiPortableCustomInteractionSchema: z.ZodTypeAny = z.lazy(() =>
659
659
  catalogInfo: QtiCatalogInfoSchema.optional(),
660
660
  customInteractionTypeIdentifier: z.string(),
661
661
  module: z.string().optional(),
662
+ /** PCI configuration properties: the element's data-* attributes, prefix stripped. */
663
+ properties: z.record(z.string(), z.string()).optional(),
662
664
  }),
663
665
  );
664
666
 
665
- export const QtiCustomInteractionSchema: z.ZodTypeAny = z.lazy(() =>
667
+ export const QtiCustomInteractionSchema: z.ZodType = z.lazy(() =>
666
668
  strictObject({
667
669
  kind: z.literal("customInteraction"),
668
670
  ...QtiCommonNodeShape,
@@ -672,7 +674,7 @@ export const QtiCustomInteractionSchema: z.ZodTypeAny = z.lazy(() =>
672
674
  }),
673
675
  );
674
676
 
675
- export const QtiDrawingInteractionSchema: z.ZodTypeAny = z.lazy(() =>
677
+ export const QtiDrawingInteractionSchema: z.ZodType = z.lazy(() =>
676
678
  strictObject({
677
679
  kind: z.literal("drawingInteraction"),
678
680
  ...QtiCommonNodeShape,
@@ -683,17 +685,19 @@ export const QtiDrawingInteractionSchema: z.ZodTypeAny = z.lazy(() =>
683
685
  }),
684
686
  );
685
687
 
686
- export const QtiEndAttemptInteractionSchema: z.ZodTypeAny = z.lazy(() =>
688
+ export const QtiEndAttemptInteractionSchema: z.ZodType = z.lazy(() =>
687
689
  strictObject({
688
690
  kind: z.literal("endAttemptInteraction"),
689
691
  ...QtiCommonNodeShape,
690
692
  responseIdentifier: QtiIdentifierSchema.optional(),
693
+ // The XSD-required button label (`title` on qti-end-attempt-interaction).
694
+ title: z.string().optional(),
691
695
  content: z.array(QtiContentFragmentSchema).optional(),
692
696
  attributes: XmlForeignAttributesSchema.optional(),
693
697
  }),
694
698
  );
695
699
 
696
- export const QtiPositionObjectStageSchema: z.ZodTypeAny = z.lazy(() =>
700
+ export const QtiPositionObjectStageSchema: z.ZodType = z.lazy(() =>
697
701
  strictObject({
698
702
  kind: z.literal("positionObjectStage"),
699
703
  id: QtiIdentifierSchema.optional(),
@@ -703,7 +707,7 @@ export const QtiPositionObjectStageSchema: z.ZodTypeAny = z.lazy(() =>
703
707
  }),
704
708
  );
705
709
 
706
- export const QtiContentFragmentSchema: z.ZodTypeAny = z.lazy(() =>
710
+ export const QtiContentFragmentSchema: z.ZodType = z.lazy(() =>
707
711
  z.union([
708
712
  z.string(),
709
713
  QtiXmlContentNodeSchema,
@@ -749,13 +753,13 @@ export const QtiContentFragmentSchema: z.ZodTypeAny = z.lazy(() =>
749
753
  ]),
750
754
  );
751
755
 
752
- export const QtiItemBodySchema: z.ZodTypeAny = z.lazy(() =>
756
+ export const QtiItemBodySchema: z.ZodType = z.lazy(() =>
753
757
  strictObject({
754
758
  content: z.array(QtiContentFragmentSchema).min(1),
755
759
  }),
756
760
  );
757
761
 
758
- export const QtiStimulusBodySchema: z.ZodTypeAny = z.lazy(() =>
762
+ export const QtiStimulusBodySchema: z.ZodType = z.lazy(() =>
759
763
  strictObject({
760
764
  ...QtiCommonNodeShape,
761
765
  content: z.array(QtiContentFragmentSchema).min(1),
@@ -827,7 +831,35 @@ export const QtiAssessmentStimulusRefSchema = strictObject({
827
831
  foreignAttributes: XmlForeignAttributesSchema.optional(),
828
832
  });
829
833
 
830
- function walkUnknown(value: unknown, visit: (node: Record<string, unknown>) => void) {
834
+ /**
835
+ * The ASI node fields the cross-validators read. Nodes arrive pre-validation
836
+ * (these refinements run during parsing), so every field stays `unknown` until
837
+ * narrowed; the declared names are the QTI vocabulary the checks are defined
838
+ * over -- anything else on a node flows through the index signature.
839
+ */
840
+ type AsiNodeView = {
841
+ kind?: unknown;
842
+ identifier?: unknown;
843
+ responseIdentifier?: unknown;
844
+ content?: unknown;
845
+ simpleChoices?: unknown;
846
+ inlineChoices?: unknown;
847
+ gapChoices?: unknown;
848
+ hotspotChoices?: unknown;
849
+ minChoices?: unknown;
850
+ maxChoices?: unknown;
851
+ minStrings?: unknown;
852
+ maxStrings?: unknown;
853
+ minAssociations?: unknown;
854
+ maxAssociations?: unknown;
855
+ minPlays?: unknown;
856
+ maxPlays?: unknown;
857
+ lowerBound?: unknown;
858
+ upperBound?: unknown;
859
+ [key: string]: unknown;
860
+ };
861
+
862
+ function walkUnknown(value: unknown, visit: (node: AsiNodeView) => void) {
831
863
  if (Array.isArray(value)) {
832
864
  for (const entry of value) {
833
865
  walkUnknown(entry, visit);
@@ -839,7 +871,7 @@ function walkUnknown(value: unknown, visit: (node: Record<string, unknown>) => v
839
871
  return;
840
872
  }
841
873
 
842
- const node = value as Record<string, unknown>;
874
+ const node = value as AsiNodeView;
843
875
  visit(node);
844
876
 
845
877
  for (const [key, child] of Object.entries(node)) {
@@ -863,8 +895,8 @@ function containsInteraction(value: unknown): boolean {
863
895
  return found;
864
896
  }
865
897
 
866
- function collectInteractionNodes(value: unknown): Array<Record<string, unknown>> {
867
- const interactions: Array<Record<string, unknown>> = [];
898
+ function collectInteractionNodes(value: unknown): AsiNodeView[] {
899
+ const interactions: AsiNodeView[] = [];
868
900
 
869
901
  walkUnknown(value, (node) => {
870
902
  const kind = typeof node.kind === "string" ? node.kind : null;
@@ -876,8 +908,8 @@ function collectInteractionNodes(value: unknown): Array<Record<string, unknown>>
876
908
  return interactions;
877
909
  }
878
910
 
879
- function collectNodesByKind(value: unknown, kinds: readonly string[]): Array<Record<string, unknown>> {
880
- const matches: Array<Record<string, unknown>> = [];
911
+ function collectNodesByKind(value: unknown, kinds: readonly string[]): AsiNodeView[] {
912
+ const matches: AsiNodeView[] = [];
881
913
  const kindSet = new Set(kinds);
882
914
 
883
915
  walkUnknown(value, (node) => {
@@ -936,14 +968,16 @@ function validateOutcomeDeclarationConventions(
936
968
  path: Array<string | number>,
937
969
  ) {
938
970
  for (const [index, declaration] of declarations.entries()) {
971
+ // The information model recommends float, but the XSD does not enforce it and the
972
+ // official corpus ships integer SCOREs — require single numeric, nothing stricter.
939
973
  if (
940
974
  ["SCORE", "MAXSCORE"].includes(declaration.identifier) &&
941
- (declaration.baseType !== "float" || declaration.cardinality !== "single")
975
+ (!["float", "integer"].includes(declaration.baseType ?? "") || declaration.cardinality !== "single")
942
976
  ) {
943
977
  addIssue(
944
978
  context,
945
979
  [...path, index],
946
- `${declaration.identifier} should have baseType 'float' and cardinality 'single'.`,
980
+ `${declaration.identifier} should be a single numeric outcome (baseType 'float' or 'integer').`,
947
981
  );
948
982
  }
949
983
 
@@ -971,7 +1005,7 @@ function addDuplicateSummaryIssue(
971
1005
  }
972
1006
 
973
1007
  function validateResponseBinding(
974
- interaction: Record<string, unknown>,
1008
+ interaction: AsiNodeView,
975
1009
  responsesById: Map<string, z.infer<typeof QtiResponseDeclarationSchema>>,
976
1010
  context: z.RefinementCtx,
977
1011
  path: Array<string | number>,
@@ -1018,8 +1052,8 @@ function validateResponseBinding(
1018
1052
  typeof choice.identifier === "string" ? [choice.identifier] : [],
1019
1053
  ),
1020
1054
  );
1021
- const minChoices = typeof interaction.minChoices === "number" ? interaction.minChoices : undefined;
1022
- const maxChoices = typeof interaction.maxChoices === "number" ? interaction.maxChoices : undefined;
1055
+ const minChoices = minOf(interaction.minChoices);
1056
+ const maxChoices = boundedMax(interaction.maxChoices);
1023
1057
  if (minChoices !== undefined && maxChoices !== undefined && minChoices > maxChoices) {
1024
1058
  addIssue(context, path, "choiceInteraction minChoices must not exceed maxChoices.");
1025
1059
  }
@@ -1052,8 +1086,8 @@ function validateResponseBinding(
1052
1086
  break;
1053
1087
  }
1054
1088
  requireBaseAndCardinality(["string", "integer", "float"], ["single", "multiple", "ordered"]);
1055
- const minStrings = typeof interaction.minStrings === "number" ? interaction.minStrings : undefined;
1056
- const maxStrings = typeof interaction.maxStrings === "number" ? interaction.maxStrings : undefined;
1089
+ const minStrings = minOf(interaction.minStrings);
1090
+ const maxStrings = boundedMax(interaction.maxStrings);
1057
1091
  if (minStrings !== undefined && maxStrings !== undefined && minStrings > maxStrings) {
1058
1092
  addIssue(context, path, "extendedTextInteraction minStrings must not exceed maxStrings.");
1059
1093
  }
@@ -1062,8 +1096,8 @@ function validateResponseBinding(
1062
1096
 
1063
1097
  case "matchInteraction": {
1064
1098
  requireBaseAndCardinality(["directedPair"], ["single", "multiple"]);
1065
- const minAssociations = typeof interaction.minAssociations === "number" ? interaction.minAssociations : undefined;
1066
- const maxAssociations = typeof interaction.maxAssociations === "number" ? interaction.maxAssociations : undefined;
1099
+ const minAssociations = minOf(interaction.minAssociations);
1100
+ const maxAssociations = boundedMax(interaction.maxAssociations);
1067
1101
  if (minAssociations !== undefined && maxAssociations !== undefined && minAssociations > maxAssociations) {
1068
1102
  addIssue(context, path, "matchInteraction minAssociations must not exceed maxAssociations.");
1069
1103
  }
@@ -1080,8 +1114,8 @@ function validateResponseBinding(
1080
1114
  typeof choice.identifier === "string" ? [choice.identifier] : [],
1081
1115
  ),
1082
1116
  );
1083
- const minAssociations = typeof interaction.minAssociations === "number" ? interaction.minAssociations : undefined;
1084
- const maxAssociations = typeof interaction.maxAssociations === "number" ? interaction.maxAssociations : undefined;
1117
+ const minAssociations = minOf(interaction.minAssociations);
1118
+ const maxAssociations = boundedMax(interaction.maxAssociations);
1085
1119
  if (minAssociations !== undefined && maxAssociations !== undefined && minAssociations > maxAssociations) {
1086
1120
  addIssue(context, path, "gapMatchInteraction minAssociations must not exceed maxAssociations.");
1087
1121
  }
@@ -1090,8 +1124,8 @@ function validateResponseBinding(
1090
1124
 
1091
1125
  case "mediaInteraction": {
1092
1126
  requireBaseAndCardinality(["integer"], ["single"]);
1093
- const minPlays = typeof interaction.minPlays === "number" ? interaction.minPlays : undefined;
1094
- const maxPlays = typeof interaction.maxPlays === "number" ? interaction.maxPlays : undefined;
1127
+ const minPlays = minOf(interaction.minPlays);
1128
+ const maxPlays = boundedMax(interaction.maxPlays);
1095
1129
  if (minPlays !== undefined && maxPlays !== undefined && minPlays > maxPlays) {
1096
1130
  addIssue(context, path, "mediaInteraction minPlays must not exceed maxPlays.");
1097
1131
  }
@@ -1139,8 +1173,8 @@ function validateResponseBinding(
1139
1173
  typeof choice.identifier === "string" ? [choice.identifier] : [],
1140
1174
  ),
1141
1175
  );
1142
- const minChoices = typeof interaction.minChoices === "number" ? interaction.minChoices : undefined;
1143
- const maxChoices = typeof interaction.maxChoices === "number" ? interaction.maxChoices : undefined;
1176
+ const minChoices = minOf(interaction.minChoices);
1177
+ const maxChoices = boundedMax(interaction.maxChoices);
1144
1178
  if (minChoices !== undefined && maxChoices !== undefined && minChoices > maxChoices) {
1145
1179
  addIssue(context, path, "hotspotInteraction minChoices must not exceed maxChoices.");
1146
1180
  }
@@ -1150,8 +1184,8 @@ function validateResponseBinding(
1150
1184
  case "associateInteraction":
1151
1185
  case "graphicAssociateInteraction": {
1152
1186
  requireBaseAndCardinality(["pair"], ["single", "multiple"]);
1153
- const minAssociations = typeof interaction.minAssociations === "number" ? interaction.minAssociations : undefined;
1154
- const maxAssociations = typeof interaction.maxAssociations === "number" ? interaction.maxAssociations : undefined;
1187
+ const minAssociations = minOf(interaction.minAssociations);
1188
+ const maxAssociations = boundedMax(interaction.maxAssociations);
1155
1189
  if (minAssociations !== undefined && maxAssociations !== undefined && minAssociations > maxAssociations) {
1156
1190
  addIssue(context, path, `${kind} minAssociations must not exceed maxAssociations.`);
1157
1191
  }
@@ -1171,8 +1205,8 @@ function validateResponseBinding(
1171
1205
  case "selectPointInteraction":
1172
1206
  case "positionObjectInteraction": {
1173
1207
  requireBaseAndCardinality(["point"], ["single", "multiple"]);
1174
- const minChoices = typeof interaction.minChoices === "number" ? interaction.minChoices : undefined;
1175
- const maxChoices = typeof interaction.maxChoices === "number" ? interaction.maxChoices : undefined;
1208
+ const minChoices = minOf(interaction.minChoices);
1209
+ const maxChoices = boundedMax(interaction.maxChoices);
1176
1210
  if (minChoices !== undefined && maxChoices !== undefined && minChoices > maxChoices) {
1177
1211
  addIssue(context, path, `${kind} minChoices must not exceed maxChoices.`);
1178
1212
  }
@@ -1191,6 +1225,13 @@ function validateResponseBinding(
1191
1225
  }
1192
1226
  }
1193
1227
 
1228
+ /**
1229
+ * Built-in session outcomes every QTI item declares implicitly. `completion_status`
1230
+ * is the snake_case authoring of the same built-in that the official corpus ships
1231
+ * (the runtime treats it as an alias when reading adaptive completion).
1232
+ */
1233
+ const builtInOutcomeIdentifiers = new Set(["completionStatus", "completion_status"]);
1234
+
1194
1235
  function validateOutcomeReferences(
1195
1236
  value: unknown,
1196
1237
  declaredOutcomes: Map<string, unknown>,
@@ -1199,12 +1240,21 @@ function validateOutcomeReferences(
1199
1240
  ) {
1200
1241
  for (const rule of collectNodesByKind(value, ["setOutcomeValue", "lookupOutcomeValue"])) {
1201
1242
  const identifier = typeof rule.identifier === "string" ? rule.identifier : undefined;
1202
- if (identifier && !declaredOutcomes.has(identifier)) {
1243
+ if (identifier && !declaredOutcomes.has(identifier) && !builtInOutcomeIdentifiers.has(identifier)) {
1203
1244
  addIssue(context, path, `Processing rule references undeclared outcome identifier '${identifier}'.`);
1204
1245
  }
1205
1246
  }
1206
1247
  }
1207
1248
 
1249
+ /** QTI max-* attributes use 0 to mean "unbounded"; bound checks must ignore it. */
1250
+ function boundedMax(value: unknown): number | undefined {
1251
+ return typeof value === "number" && value > 0 ? value : undefined;
1252
+ }
1253
+
1254
+ function minOf(value: unknown): number | undefined {
1255
+ return typeof value === "number" ? value : undefined;
1256
+ }
1257
+
1208
1258
  function validateTemplateAndResponseReferences(
1209
1259
  value: unknown,
1210
1260
  declaredTemplates: Map<string, unknown>,
@@ -1227,7 +1277,7 @@ function validateTemplateAndResponseReferences(
1227
1277
  }
1228
1278
  }
1229
1279
 
1230
- export const QtiAssessmentSectionRawSchema: z.ZodTypeAny = z.lazy(() =>
1280
+ export const QtiAssessmentSectionRawSchema: z.ZodType = z.lazy(() =>
1231
1281
  strictObject({
1232
1282
  identifier: QtiIdentifierSchema,
1233
1283
  required: z.boolean().optional(),
@@ -1258,7 +1308,7 @@ export const QtiAssessmentSectionRawSchema: z.ZodTypeAny = z.lazy(() =>
1258
1308
  }),
1259
1309
  );
1260
1310
 
1261
- export const QtiAssessmentSectionSchema: z.ZodTypeAny = QtiAssessmentSectionRawSchema.superRefine(
1311
+ export const QtiAssessmentSectionSchema: z.ZodType = QtiAssessmentSectionRawSchema.superRefine(
1262
1312
  (value: unknown, context) => {
1263
1313
  const section = value as QtiSectionValidationValue;
1264
1314
 
@@ -1287,7 +1337,7 @@ export const QtiAssessmentSectionSchema: z.ZodTypeAny = QtiAssessmentSectionRawS
1287
1337
  },
1288
1338
  );
1289
1339
 
1290
- export const QtiTestPartSchema: z.ZodTypeAny = z.lazy(() =>
1340
+ export const QtiTestPartSchema: z.ZodType = z.lazy(() =>
1291
1341
  strictObject({
1292
1342
  identifier: QtiIdentifierSchema,
1293
1343
  title: z.string().optional(),
@@ -1305,7 +1355,7 @@ export const QtiTestPartSchema: z.ZodTypeAny = z.lazy(() =>
1305
1355
  }),
1306
1356
  );
1307
1357
 
1308
- export const QtiAssessmentStimulusSchema: z.ZodTypeAny = z.lazy(() =>
1358
+ export const QtiAssessmentStimulusSchema: z.ZodType = z.lazy(() =>
1309
1359
  strictObject({
1310
1360
  identifier: QtiIdentifierSchema,
1311
1361
  title: z.string(),
@@ -1319,7 +1369,7 @@ export const QtiAssessmentStimulusSchema: z.ZodTypeAny = z.lazy(() =>
1319
1369
  }),
1320
1370
  );
1321
1371
 
1322
- export const QtiAssessmentItemRawSchema: z.ZodTypeAny = z.lazy(() =>
1372
+ export const QtiAssessmentItemRawSchema: z.ZodType = z.lazy(() =>
1323
1373
  strictObject({
1324
1374
  identifier: QtiIdentifierSchema,
1325
1375
  title: z.string(),
@@ -1345,92 +1395,86 @@ export const QtiAssessmentItemRawSchema: z.ZodTypeAny = z.lazy(() =>
1345
1395
  }),
1346
1396
  );
1347
1397
 
1348
- export const QtiAssessmentItemSchema: z.ZodTypeAny = QtiAssessmentItemRawSchema.superRefine(
1349
- (value: unknown, context) => {
1350
- const item = value as QtiItemValidationValue;
1351
- const contextDeclarations = asArray(item.contextDeclarations);
1352
- const responseDeclarationsList = asArray(item.responseDeclarations);
1353
- const outcomeDeclarationsList = asArray(item.outcomeDeclarations);
1354
- const templateDeclarationsList = asArray(item.templateDeclarations);
1355
- const modalFeedbacks = asArray(item.modalFeedbacks);
1356
-
1357
- const variableDeclarations = [
1358
- ...contextDeclarations,
1359
- ...responseDeclarationsList,
1360
- ...outcomeDeclarationsList,
1361
- ...templateDeclarationsList,
1362
- ];
1363
-
1364
- addDuplicateSummaryIssue(
1365
- context,
1366
- [],
1367
- "assessment item variable declaration identifiers",
1368
- variableDeclarations.map((declaration) => declaration.identifier),
1369
- );
1370
-
1371
- validateOutcomeDeclarationConventions(outcomeDeclarationsList, context, ["outcomeDeclarations"]);
1372
-
1373
- const responseDeclarations = new Map(
1374
- responseDeclarationsList.map((declaration) => [declaration.identifier, declaration]),
1375
- );
1376
- const outcomeDeclarations = new Map(
1377
- outcomeDeclarationsList.map((declaration) => [declaration.identifier, declaration]),
1378
- );
1379
- const templateDeclarations = new Map(
1380
- templateDeclarationsList.map((declaration) => [declaration.identifier, declaration]),
1381
- );
1382
-
1383
- const interactions = collectInteractionNodes(item.itemBody?.content);
1384
- const responseBindings = new Map<string, string[]>();
1398
+ export const QtiAssessmentItemSchema: z.ZodType = QtiAssessmentItemRawSchema.superRefine((value: unknown, context) => {
1399
+ const item = value as QtiItemValidationValue;
1400
+ const contextDeclarations = asArray(item.contextDeclarations);
1401
+ const responseDeclarationsList = asArray(item.responseDeclarations);
1402
+ const outcomeDeclarationsList = asArray(item.outcomeDeclarations);
1403
+ const templateDeclarationsList = asArray(item.templateDeclarations);
1404
+ const modalFeedbacks = asArray(item.modalFeedbacks);
1405
+
1406
+ const variableDeclarations = [
1407
+ ...contextDeclarations,
1408
+ ...responseDeclarationsList,
1409
+ ...outcomeDeclarationsList,
1410
+ ...templateDeclarationsList,
1411
+ ];
1412
+
1413
+ addDuplicateSummaryIssue(
1414
+ context,
1415
+ [],
1416
+ "assessment item variable declaration identifiers",
1417
+ variableDeclarations.map((declaration) => declaration.identifier),
1418
+ );
1419
+
1420
+ validateOutcomeDeclarationConventions(outcomeDeclarationsList, context, ["outcomeDeclarations"]);
1421
+
1422
+ const responseDeclarations = new Map(
1423
+ responseDeclarationsList.map((declaration) => [declaration.identifier, declaration]),
1424
+ );
1425
+ const outcomeDeclarations = new Map(
1426
+ outcomeDeclarationsList.map((declaration) => [declaration.identifier, declaration]),
1427
+ );
1428
+ const templateDeclarations = new Map(
1429
+ templateDeclarationsList.map((declaration) => [declaration.identifier, declaration]),
1430
+ );
1431
+
1432
+ const interactions = collectInteractionNodes(item.itemBody?.content);
1433
+ const responseBindings = new Map<string, string[]>();
1434
+
1435
+ for (const interaction of interactions) {
1436
+ const responseIdentifier =
1437
+ typeof interaction.responseIdentifier === "string" ? interaction.responseIdentifier : undefined;
1438
+ if (responseIdentifier) {
1439
+ const boundKinds = responseBindings.get(responseIdentifier) ?? [];
1440
+ boundKinds.push(String(interaction.kind));
1441
+ responseBindings.set(responseIdentifier, boundKinds);
1442
+ }
1385
1443
 
1386
- for (const interaction of interactions) {
1387
- const responseIdentifier =
1388
- typeof interaction.responseIdentifier === "string" ? interaction.responseIdentifier : undefined;
1389
- if (responseIdentifier) {
1390
- const boundKinds = responseBindings.get(responseIdentifier) ?? [];
1391
- boundKinds.push(String(interaction.kind));
1392
- responseBindings.set(responseIdentifier, boundKinds);
1393
- }
1444
+ validateResponseBinding(interaction, responseDeclarations, context, ["itemBody"]);
1445
+ }
1394
1446
 
1395
- validateResponseBinding(interaction, responseDeclarations, context, ["itemBody"]);
1447
+ for (const [identifier, kinds] of responseBindings) {
1448
+ if (kinds.length > 1) {
1449
+ addIssue(
1450
+ context,
1451
+ ["itemBody"],
1452
+ `Response identifier '${identifier}' is bound by multiple interactions: ${kinds.join(", ")}.`,
1453
+ );
1396
1454
  }
1455
+ }
1397
1456
 
1398
- for (const [identifier, kinds] of responseBindings) {
1399
- if (kinds.length > 1) {
1400
- addIssue(
1401
- context,
1402
- ["itemBody"],
1403
- `Response identifier '${identifier}' is bound by multiple interactions: ${kinds.join(", ")}.`,
1404
- );
1405
- }
1457
+ for (const [index, feedback] of modalFeedbacks.entries()) {
1458
+ if (!outcomeDeclarations.has(feedback.outcomeIdentifier)) {
1459
+ addIssue(
1460
+ context,
1461
+ ["modalFeedbacks", index, "outcomeIdentifier"],
1462
+ `modalFeedback references undeclared outcome identifier '${feedback.outcomeIdentifier}'.`,
1463
+ );
1406
1464
  }
1407
1465
 
1408
- for (const [index, feedback] of modalFeedbacks.entries()) {
1409
- if (!outcomeDeclarations.has(feedback.outcomeIdentifier)) {
1410
- addIssue(
1411
- context,
1412
- ["modalFeedbacks", index, "outcomeIdentifier"],
1413
- `modalFeedback references undeclared outcome identifier '${feedback.outcomeIdentifier}'.`,
1414
- );
1415
- }
1416
-
1417
- if (containsInteraction(feedback.content)) {
1418
- addIssue(context, ["modalFeedbacks", index, "content"], "modalFeedback content must not contain interactions.");
1419
- }
1466
+ if (containsInteraction(feedback.content)) {
1467
+ addIssue(context, ["modalFeedbacks", index, "content"], "modalFeedback content must not contain interactions.");
1420
1468
  }
1469
+ }
1421
1470
 
1422
- validateOutcomeReferences(item.responseProcessing, outcomeDeclarations, context, ["responseProcessing"]);
1423
- validateTemplateAndResponseReferences(
1424
- item.templateProcessing,
1425
- templateDeclarations,
1426
- responseDeclarations,
1427
- context,
1428
- ["templateProcessing"],
1429
- );
1430
- },
1431
- );
1471
+ validateOutcomeReferences(item.responseProcessing, outcomeDeclarations, context, ["responseProcessing"]);
1472
+ validateTemplateAndResponseReferences(item.templateProcessing, templateDeclarations, responseDeclarations, context, [
1473
+ "templateProcessing",
1474
+ ]);
1475
+ });
1432
1476
 
1433
- export const QtiAssessmentTestRawSchema: z.ZodTypeAny = z.lazy(() =>
1477
+ export const QtiAssessmentTestRawSchema: z.ZodType = z.lazy(() =>
1434
1478
  strictObject({
1435
1479
  identifier: QtiIdentifierSchema,
1436
1480
  title: z.string(),
@@ -1449,47 +1493,45 @@ export const QtiAssessmentTestRawSchema: z.ZodTypeAny = z.lazy(() =>
1449
1493
  }),
1450
1494
  );
1451
1495
 
1452
- export const QtiAssessmentTestSchema: z.ZodTypeAny = QtiAssessmentTestRawSchema.superRefine(
1453
- (value: unknown, context) => {
1454
- const testValue = value as QtiTestValidationValue;
1455
- const contextDeclarations = asArray(testValue.contextDeclarations);
1456
- const outcomeDeclarationsList = asArray(testValue.outcomeDeclarations);
1457
- const testParts = asArray(testValue.testParts);
1458
- const testFeedbacks = asArray(testValue.testFeedbacks);
1459
-
1460
- addDuplicateSummaryIssue(context, [], "assessment test variable declaration identifiers", [
1461
- ...contextDeclarations.map((declaration) => declaration.identifier),
1462
- ...outcomeDeclarationsList.map((declaration) => declaration.identifier),
1463
- ]);
1464
-
1465
- addDuplicateSummaryIssue(
1466
- context,
1467
- ["testParts"],
1468
- "test part identifiers",
1469
- testParts.map((testPart) => testPart.identifier),
1470
- );
1496
+ export const QtiAssessmentTestSchema: z.ZodType = QtiAssessmentTestRawSchema.superRefine((value: unknown, context) => {
1497
+ const testValue = value as QtiTestValidationValue;
1498
+ const contextDeclarations = asArray(testValue.contextDeclarations);
1499
+ const outcomeDeclarationsList = asArray(testValue.outcomeDeclarations);
1500
+ const testParts = asArray(testValue.testParts);
1501
+ const testFeedbacks = asArray(testValue.testFeedbacks);
1471
1502
 
1472
- validateOutcomeDeclarationConventions(outcomeDeclarationsList, context, ["outcomeDeclarations"]);
1503
+ addDuplicateSummaryIssue(context, [], "assessment test variable declaration identifiers", [
1504
+ ...contextDeclarations.map((declaration) => declaration.identifier),
1505
+ ...outcomeDeclarationsList.map((declaration) => declaration.identifier),
1506
+ ]);
1473
1507
 
1474
- const outcomes = new Map(outcomeDeclarationsList.map((declaration) => [declaration.identifier, declaration]));
1508
+ addDuplicateSummaryIssue(
1509
+ context,
1510
+ ["testParts"],
1511
+ "test part identifiers",
1512
+ testParts.map((testPart) => testPart.identifier),
1513
+ );
1475
1514
 
1476
- for (const [index, feedback] of testFeedbacks.entries()) {
1477
- if (!outcomes.has(feedback.outcomeIdentifier)) {
1478
- addIssue(
1479
- context,
1480
- ["testFeedbacks", index, "outcomeIdentifier"],
1481
- `testFeedback references undeclared outcome identifier '${feedback.outcomeIdentifier}'.`,
1482
- );
1483
- }
1515
+ validateOutcomeDeclarationConventions(outcomeDeclarationsList, context, ["outcomeDeclarations"]);
1484
1516
 
1485
- if (containsInteraction(feedback.content)) {
1486
- addIssue(context, ["testFeedbacks", index, "content"], "testFeedback content must not contain interactions.");
1487
- }
1517
+ const outcomes = new Map(outcomeDeclarationsList.map((declaration) => [declaration.identifier, declaration]));
1518
+
1519
+ for (const [index, feedback] of testFeedbacks.entries()) {
1520
+ if (!outcomes.has(feedback.outcomeIdentifier)) {
1521
+ addIssue(
1522
+ context,
1523
+ ["testFeedbacks", index, "outcomeIdentifier"],
1524
+ `testFeedback references undeclared outcome identifier '${feedback.outcomeIdentifier}'.`,
1525
+ );
1488
1526
  }
1489
1527
 
1490
- validateOutcomeReferences(testValue.outcomeProcessing, outcomes, context, ["outcomeProcessing"]);
1491
- },
1492
- );
1528
+ if (containsInteraction(feedback.content)) {
1529
+ addIssue(context, ["testFeedbacks", index, "content"], "testFeedback content must not contain interactions.");
1530
+ }
1531
+ }
1532
+
1533
+ validateOutcomeReferences(testValue.outcomeProcessing, outcomes, context, ["outcomeProcessing"]);
1534
+ });
1493
1535
  // Inferred types from exported Zod validators.
1494
1536
  export type QtiPrintedVariable = z.infer<typeof QtiPrintedVariableSchema>;
1495
1537
  export type QtiInteractionModule = z.infer<typeof QtiInteractionModuleSchema>;