@longsightgroup/qti3-core 0.9.1 → 0.9.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 (244) hide show
  1. package/README.md +59 -3
  2. package/dist/adaptive-turn-materializer.d.ts +14 -0
  3. package/dist/adaptive-turn-materializer.d.ts.map +1 -0
  4. package/dist/adaptive-turn-materializer.js +56 -0
  5. package/dist/adaptive-turn-materializer.js.map +1 -0
  6. package/dist/adaptive-turn.d.ts +23 -0
  7. package/dist/adaptive-turn.d.ts.map +1 -0
  8. package/dist/adaptive-turn.js +60 -0
  9. package/dist/adaptive-turn.js.map +1 -0
  10. package/dist/assert-never.d.ts +3 -0
  11. package/dist/assert-never.d.ts.map +1 -0
  12. package/dist/assert-never.js +5 -0
  13. package/dist/assert-never.js.map +1 -0
  14. package/dist/attempt-state-constants.d.ts +6 -0
  15. package/dist/attempt-state-constants.d.ts.map +1 -0
  16. package/dist/attempt-state-constants.js +6 -0
  17. package/dist/attempt-state-constants.js.map +1 -0
  18. package/dist/attempt-state.d.ts +5 -0
  19. package/dist/attempt-state.d.ts.map +1 -0
  20. package/dist/attempt-state.js +190 -0
  21. package/dist/attempt-state.js.map +1 -0
  22. package/dist/catalog.js +1 -1
  23. package/dist/catalog.js.map +1 -1
  24. package/dist/content-text.d.ts.map +1 -1
  25. package/dist/content-text.js +3 -0
  26. package/dist/content-text.js.map +1 -1
  27. package/dist/custom-operators.d.ts +13 -0
  28. package/dist/custom-operators.d.ts.map +1 -0
  29. package/dist/custom-operators.js +2 -0
  30. package/dist/custom-operators.js.map +1 -0
  31. package/dist/delivery-redaction.d.ts +50 -0
  32. package/dist/delivery-redaction.d.ts.map +1 -0
  33. package/dist/delivery-redaction.js +176 -0
  34. package/dist/delivery-redaction.js.map +1 -0
  35. package/dist/delivery-security.d.ts +3 -18
  36. package/dist/delivery-security.d.ts.map +1 -1
  37. package/dist/delivery-security.js +35 -189
  38. package/dist/delivery-security.js.map +1 -1
  39. package/dist/index.d.ts +4 -1
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +3 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/parser-custom-interactions.js +1 -1
  44. package/dist/parser-custom-interactions.js.map +1 -1
  45. package/dist/parser-declarations.d.ts.map +1 -1
  46. package/dist/parser-declarations.js +4 -4
  47. package/dist/parser-declarations.js.map +1 -1
  48. package/dist/parser-item-metadata.d.ts.map +1 -1
  49. package/dist/parser-item-metadata.js +8 -7
  50. package/dist/parser-item-metadata.js.map +1 -1
  51. package/dist/parser-processing.js +2 -2
  52. package/dist/parser-processing.js.map +1 -1
  53. package/dist/parser-values.d.ts +5 -1
  54. package/dist/parser-values.d.ts.map +1 -1
  55. package/dist/parser-values.js +33 -0
  56. package/dist/parser-values.js.map +1 -1
  57. package/dist/processing-evaluator-boolean.d.ts +8 -0
  58. package/dist/processing-evaluator-boolean.d.ts.map +1 -0
  59. package/dist/processing-evaluator-boolean.js +47 -0
  60. package/dist/processing-evaluator-boolean.js.map +1 -0
  61. package/dist/processing-evaluator-collection.d.ts +12 -0
  62. package/dist/processing-evaluator-collection.d.ts.map +1 -0
  63. package/dist/processing-evaluator-collection.js +35 -0
  64. package/dist/processing-evaluator-collection.js.map +1 -0
  65. package/dist/processing-evaluator-comparison.d.ts +8 -0
  66. package/dist/processing-evaluator-comparison.d.ts.map +1 -0
  67. package/dist/processing-evaluator-comparison.js +88 -0
  68. package/dist/processing-evaluator-comparison.js.map +1 -0
  69. package/dist/processing-evaluator-custom.d.ts +8 -0
  70. package/dist/processing-evaluator-custom.d.ts.map +1 -0
  71. package/dist/processing-evaluator-custom.js +14 -0
  72. package/dist/processing-evaluator-custom.js.map +1 -0
  73. package/dist/processing-evaluator-geometry.d.ts +8 -0
  74. package/dist/processing-evaluator-geometry.d.ts.map +1 -0
  75. package/dist/processing-evaluator-geometry.js +21 -0
  76. package/dist/processing-evaluator-geometry.js.map +1 -0
  77. package/dist/processing-evaluator-numeric.d.ts +8 -0
  78. package/dist/processing-evaluator-numeric.d.ts.map +1 -0
  79. package/dist/processing-evaluator-numeric.js +112 -0
  80. package/dist/processing-evaluator-numeric.js.map +1 -0
  81. package/dist/processing-evaluator-string.d.ts +8 -0
  82. package/dist/processing-evaluator-string.d.ts.map +1 -0
  83. package/dist/processing-evaluator-string.js +33 -0
  84. package/dist/processing-evaluator-string.js.map +1 -0
  85. package/dist/processing-evaluator-variable.d.ts +8 -0
  86. package/dist/processing-evaluator-variable.d.ts.map +1 -0
  87. package/dist/processing-evaluator-variable.js +39 -0
  88. package/dist/processing-evaluator-variable.js.map +1 -0
  89. package/dist/processing-evaluator.d.ts +22 -0
  90. package/dist/processing-evaluator.d.ts.map +1 -0
  91. package/dist/processing-evaluator.js +137 -0
  92. package/dist/processing-evaluator.js.map +1 -0
  93. package/dist/processing-expression-children.d.ts +3 -0
  94. package/dist/processing-expression-children.d.ts.map +1 -0
  95. package/dist/processing-expression-children.js +68 -0
  96. package/dist/processing-expression-children.js.map +1 -0
  97. package/dist/processing-mapping.d.ts +13 -0
  98. package/dist/processing-mapping.d.ts.map +1 -0
  99. package/dist/processing-mapping.js +120 -0
  100. package/dist/processing-mapping.js.map +1 -0
  101. package/dist/processing-operators.d.ts +11 -0
  102. package/dist/processing-operators.d.ts.map +1 -0
  103. package/dist/processing-operators.js +178 -0
  104. package/dist/processing-operators.js.map +1 -0
  105. package/dist/processing-random.d.ts +2 -0
  106. package/dist/processing-random.d.ts.map +1 -0
  107. package/dist/processing-random.js +16 -0
  108. package/dist/processing-random.js.map +1 -0
  109. package/dist/processing-state.d.ts +8 -0
  110. package/dist/processing-state.d.ts.map +1 -0
  111. package/dist/processing-state.js +44 -0
  112. package/dist/processing-state.js.map +1 -0
  113. package/dist/processing-templates.d.ts +3 -0
  114. package/dist/processing-templates.d.ts.map +1 -0
  115. package/dist/processing-templates.js +12 -0
  116. package/dist/processing-templates.js.map +1 -0
  117. package/dist/processing-values.d.ts +15 -0
  118. package/dist/processing-values.d.ts.map +1 -0
  119. package/dist/processing-values.js +150 -0
  120. package/dist/processing-values.js.map +1 -0
  121. package/dist/processing-variables.d.ts +7 -0
  122. package/dist/processing-variables.d.ts.map +1 -0
  123. package/dist/processing-variables.js +40 -0
  124. package/dist/processing-variables.js.map +1 -0
  125. package/dist/serializer-processing-expressions.d.ts.map +1 -1
  126. package/dist/serializer-processing-expressions.js +2 -8
  127. package/dist/serializer-processing-expressions.js.map +1 -1
  128. package/dist/serializer-processing-xml.js +2 -2
  129. package/dist/serializer-processing-xml.js.map +1 -1
  130. package/dist/serializer-processing.d.ts.map +1 -1
  131. package/dist/serializer-processing.js +3 -0
  132. package/dist/serializer-processing.js.map +1 -1
  133. package/dist/server-scoring.d.ts +4 -14
  134. package/dist/server-scoring.d.ts.map +1 -1
  135. package/dist/server-scoring.js +19 -157
  136. package/dist/server-scoring.js.map +1 -1
  137. package/dist/session.d.ts +5 -14
  138. package/dist/session.d.ts.map +1 -1
  139. package/dist/session.js +76 -1158
  140. package/dist/session.js.map +1 -1
  141. package/dist/shared-vocabulary-coverage-policy.js +1 -1
  142. package/dist/shared-vocabulary-coverage-policy.js.map +1 -1
  143. package/dist/shared-vocabulary-generated-families.js +1 -1
  144. package/dist/shared-vocabulary-generated-families.js.map +1 -1
  145. package/dist/shared-vocabulary-support.js +3 -3
  146. package/dist/shared-vocabulary-support.js.map +1 -1
  147. package/dist/shared-vocabulary.d.ts.map +1 -1
  148. package/dist/shared-vocabulary.js +4 -3
  149. package/dist/shared-vocabulary.js.map +1 -1
  150. package/dist/support-evidence.d.ts +22 -0
  151. package/dist/support-evidence.d.ts.map +1 -0
  152. package/dist/support-evidence.js +93 -0
  153. package/dist/support-evidence.js.map +1 -0
  154. package/dist/support.d.ts.map +1 -1
  155. package/dist/support.js +38 -47
  156. package/dist/support.js.map +1 -1
  157. package/dist/trusted-item-session.d.ts +49 -0
  158. package/dist/trusted-item-session.d.ts.map +1 -0
  159. package/dist/trusted-item-session.js +261 -0
  160. package/dist/trusted-item-session.js.map +1 -0
  161. package/dist/tts.d.ts.map +1 -1
  162. package/dist/tts.js +16 -2
  163. package/dist/tts.js.map +1 -1
  164. package/dist/validation-geometry.d.ts +8 -0
  165. package/dist/validation-geometry.d.ts.map +1 -0
  166. package/dist/validation-geometry.js +187 -0
  167. package/dist/validation-geometry.js.map +1 -0
  168. package/dist/validation-interactions.d.ts +3 -0
  169. package/dist/validation-interactions.d.ts.map +1 -0
  170. package/dist/validation-interactions.js +644 -0
  171. package/dist/validation-interactions.js.map +1 -0
  172. package/dist/validation-primitives.d.ts +11 -0
  173. package/dist/validation-primitives.d.ts.map +1 -0
  174. package/dist/validation-primitives.js +48 -0
  175. package/dist/validation-primitives.js.map +1 -0
  176. package/dist/validation-processing.d.ts +4 -0
  177. package/dist/validation-processing.d.ts.map +1 -0
  178. package/dist/validation-processing.js +569 -0
  179. package/dist/validation-processing.js.map +1 -0
  180. package/dist/validation-shared-vocabulary-layout.d.ts +10 -0
  181. package/dist/validation-shared-vocabulary-layout.d.ts.map +1 -0
  182. package/dist/validation-shared-vocabulary-layout.js +102 -0
  183. package/dist/validation-shared-vocabulary-layout.js.map +1 -0
  184. package/dist/validation.d.ts.map +1 -1
  185. package/dist/validation.js +27 -1641
  186. package/dist/validation.js.map +1 -1
  187. package/dist/xml.js +1 -1
  188. package/dist/xml.js.map +1 -1
  189. package/package.json +4 -2
  190. package/src/adaptive-turn-materializer.ts +85 -0
  191. package/src/adaptive-turn.ts +109 -0
  192. package/src/assert-never.ts +4 -0
  193. package/src/attempt-state-constants.ts +5 -0
  194. package/src/attempt-state.ts +245 -0
  195. package/src/catalog.ts +1 -1
  196. package/src/content-text.ts +3 -0
  197. package/src/custom-operators.ts +12 -0
  198. package/src/delivery-redaction.ts +268 -0
  199. package/src/delivery-security.ts +59 -245
  200. package/src/index.ts +9 -0
  201. package/src/parser-custom-interactions.ts +1 -1
  202. package/src/parser-declarations.ts +4 -3
  203. package/src/parser-item-metadata.ts +8 -7
  204. package/src/parser-processing.ts +2 -2
  205. package/src/parser-values.ts +36 -0
  206. package/src/processing-evaluator-boolean.ts +48 -0
  207. package/src/processing-evaluator-collection.ts +50 -0
  208. package/src/processing-evaluator-comparison.ts +106 -0
  209. package/src/processing-evaluator-custom.ts +20 -0
  210. package/src/processing-evaluator-geometry.ts +26 -0
  211. package/src/processing-evaluator-numeric.ts +155 -0
  212. package/src/processing-evaluator-string.ts +60 -0
  213. package/src/processing-evaluator-variable.ts +81 -0
  214. package/src/processing-evaluator.ts +173 -0
  215. package/src/processing-expression-children.ts +69 -0
  216. package/src/processing-mapping.ts +153 -0
  217. package/src/processing-operators.ts +184 -0
  218. package/src/processing-random.ts +16 -0
  219. package/src/processing-state.ts +69 -0
  220. package/src/processing-templates.ts +12 -0
  221. package/src/processing-values.ts +162 -0
  222. package/src/processing-variables.ts +83 -0
  223. package/src/serializer-processing-expressions.ts +2 -12
  224. package/src/serializer-processing-xml.ts +2 -2
  225. package/src/serializer-processing.ts +3 -0
  226. package/src/server-scoring.ts +30 -226
  227. package/src/session.ts +128 -2006
  228. package/src/shared-vocabulary-coverage-policy.ts +1 -1
  229. package/src/shared-vocabulary-generated-families.ts +1 -1
  230. package/src/shared-vocabulary-support.ts +3 -3
  231. package/src/shared-vocabulary.ts +3 -2
  232. package/src/support-evidence.ts +107 -0
  233. package/src/support.ts +68 -54
  234. package/src/trusted-item-session.ts +429 -0
  235. package/src/tts.ts +29 -15
  236. package/src/validation-geometry.ts +204 -0
  237. package/src/validation-interactions.ts +831 -0
  238. package/src/validation-primitives.ts +63 -0
  239. package/src/validation-processing.ts +785 -0
  240. package/src/validation-shared-vocabulary-layout.ts +117 -0
  241. package/src/validation.ts +44 -2077
  242. package/src/xml.ts +1 -1
  243. package/src/interaction-test-fixtures.ts +0 -44
  244. package/src/serializer-processing.fixtures.ts +0 -469
@@ -0,0 +1,644 @@
1
+ import { assertNever } from "./assert-never.js";
2
+ import { isValidQtiPatternMask } from "./pattern-mask.js";
3
+ import { validateInteractionSharedVocabulary } from "./shared-vocabulary-interaction-validation.js";
4
+ import { qtiValueToStringList } from "./value-format.js";
5
+ import { validateGraphicHotspotObjectDimensions, validateHotspotGeometry, } from "./validation-geometry.js";
6
+ import { isBooleanAttribute, isFiniteNumber, isNonNegativeInteger, requireIdentifier, } from "./validation-primitives.js";
7
+ export function validateInteractions(item, diagnostics) {
8
+ const responseDeclarations = new Map(item.responseDeclarations.map((declaration) => [declaration.identifier, declaration]));
9
+ const responseIdentifiers = new Set(responseDeclarations.keys());
10
+ for (const interaction of item.interactions) {
11
+ validateInteractionResponseReference(interaction, responseIdentifiers, diagnostics);
12
+ validateInteractionResponseShape(interaction, diagnostics);
13
+ validateInteractionSharedVocabulary(interaction, diagnostics);
14
+ validateInteractionChoices(interaction, diagnostics);
15
+ validateInteractionChildren(interaction, diagnostics);
16
+ validateInteractionRequiredAttributes(interaction, diagnostics);
17
+ validatePortableCustomInteraction(interaction, item, diagnostics);
18
+ validateInteractionLimitAttributes(interaction, diagnostics);
19
+ validatePatternMaskAttribute(interaction, diagnostics);
20
+ validateGraphicHotspotObjectDimensions(interaction, diagnostics);
21
+ validateCorrectResponseReferences(interaction, interaction.responseIdentifier
22
+ ? responseDeclarations.get(interaction.responseIdentifier)
23
+ : undefined, diagnostics);
24
+ validateMappingReferences(interaction, interaction.responseIdentifier
25
+ ? responseDeclarations.get(interaction.responseIdentifier)
26
+ : undefined, diagnostics);
27
+ }
28
+ }
29
+ function validateInteractionResponseReference(interaction, responseIdentifiers, diagnostics) {
30
+ if (!interaction.responseIdentifier) {
31
+ if (interaction.type !== "endAttempt") {
32
+ diagnostics.push({
33
+ code: "interaction.responseIdentifier",
34
+ severity: "error",
35
+ message: `${interaction.qtiName} is missing response-identifier.`,
36
+ path: interaction.source?.path,
37
+ source: interaction.source,
38
+ });
39
+ }
40
+ return;
41
+ }
42
+ if (!responseIdentifiers.has(interaction.responseIdentifier)) {
43
+ diagnostics.push({
44
+ code: "interaction.responseIdentifier.reference",
45
+ severity: "error",
46
+ message: `${interaction.qtiName} references missing response declaration ${interaction.responseIdentifier}.`,
47
+ path: interaction.source?.path,
48
+ source: interaction.source,
49
+ });
50
+ }
51
+ }
52
+ function validateInteractionResponseShape(interaction, diagnostics) {
53
+ const expected = expectedResponseShape(interaction);
54
+ if (!expected)
55
+ return;
56
+ if (interaction.responseCardinality &&
57
+ !expected.cardinalities.includes(interaction.responseCardinality)) {
58
+ diagnostics.push({
59
+ code: "interaction.cardinality",
60
+ severity: "error",
61
+ message: `${interaction.qtiName} expects ${expected.cardinalities.join(" or ")} cardinality, got ${interaction.responseCardinality}.`,
62
+ path: interaction.source?.path,
63
+ source: interaction.source,
64
+ });
65
+ }
66
+ if (interaction.responseBaseType && !expected.baseTypes.includes(interaction.responseBaseType)) {
67
+ diagnostics.push({
68
+ code: "interaction.baseType",
69
+ severity: "error",
70
+ message: `${interaction.qtiName} expects ${expected.baseTypes.join(" or ")} base type, got ${interaction.responseBaseType}.`,
71
+ path: interaction.source?.path,
72
+ source: interaction.source,
73
+ });
74
+ }
75
+ }
76
+ function validateInteractionChoices(interaction, diagnostics) {
77
+ const identifiers = new Set();
78
+ for (const choice of interaction.choices) {
79
+ requireIdentifier(choice.qtiName, choice.attributes.identifier, diagnostics, choice.source);
80
+ if (!choice.identifier)
81
+ continue;
82
+ if (identifiers.has(choice.identifier)) {
83
+ diagnostics.push({
84
+ code: "choice.identifier.duplicate",
85
+ severity: "error",
86
+ message: `${interaction.qtiName} has duplicate choice identifier ${choice.identifier}.`,
87
+ path: choice.source?.path ?? interaction.source?.path,
88
+ source: choice.source ?? interaction.source,
89
+ });
90
+ }
91
+ identifiers.add(choice.identifier);
92
+ }
93
+ if (needsChoices(interaction) &&
94
+ interaction.choices.filter((choice) => choice.role !== "gap").length === 0) {
95
+ diagnostics.push({
96
+ code: "interaction.choices.required",
97
+ severity: "error",
98
+ message: `${interaction.qtiName} requires at least one choice.`,
99
+ path: interaction.source?.path,
100
+ source: interaction.source,
101
+ });
102
+ }
103
+ for (const choice of interaction.choices) {
104
+ validateChoiceLimitAttributes(choice, diagnostics);
105
+ }
106
+ }
107
+ function validateCorrectResponseReferences(interaction, declaration, diagnostics) {
108
+ if (!declaration || declaration.correctResponse === null)
109
+ return;
110
+ if (declaration.baseType !== "identifier" &&
111
+ declaration.baseType !== "pair" &&
112
+ declaration.baseType !== "directedPair") {
113
+ return;
114
+ }
115
+ const identifiers = new Set(interaction.choices
116
+ .map((choice) => choice.identifier)
117
+ .filter((identifier) => identifier.length > 0));
118
+ if (identifiers.size === 0)
119
+ return;
120
+ for (const value of responseValues(declaration.correctResponse)) {
121
+ if (declaration.baseType === "identifier") {
122
+ if (identifiers.has(value))
123
+ continue;
124
+ invalidCorrectResponseReference(interaction, declaration, value, diagnostics);
125
+ continue;
126
+ }
127
+ const parts = value.trim().split(/\s+/);
128
+ if (parts.length !== 2 || parts.some((part) => !identifiers.has(part))) {
129
+ invalidCorrectResponseReference(interaction, declaration, value, diagnostics);
130
+ }
131
+ }
132
+ }
133
+ function responseValues(value) {
134
+ return qtiValueToStringList(value);
135
+ }
136
+ function invalidCorrectResponseReference(interaction, declaration, value, diagnostics) {
137
+ diagnostics.push({
138
+ code: "response.correctResponse.reference",
139
+ severity: "error",
140
+ message: `Response declaration ${declaration.identifier} correct response ${value} does not reference choices in ${interaction.qtiName}.`,
141
+ path: declaration.source?.path,
142
+ source: declaration.source,
143
+ });
144
+ }
145
+ function validateMappingReferences(interaction, declaration, diagnostics) {
146
+ if (!declaration?.mapping)
147
+ return;
148
+ if (declaration.baseType !== "identifier" &&
149
+ declaration.baseType !== "pair" &&
150
+ declaration.baseType !== "directedPair") {
151
+ return;
152
+ }
153
+ const identifiers = new Set(interaction.choices
154
+ .map((choice) => choice.identifier)
155
+ .filter((identifier) => identifier.length > 0));
156
+ if (identifiers.size === 0)
157
+ return;
158
+ for (const entry of declaration.mapping.entries) {
159
+ const mapKey = entry.mapKey;
160
+ if (!mapKey)
161
+ continue;
162
+ if (declaration.baseType === "identifier") {
163
+ if (identifiers.has(mapKey))
164
+ continue;
165
+ invalidMappingReference(interaction, declaration, entry, diagnostics);
166
+ continue;
167
+ }
168
+ const parts = mapKey.trim().split(/\s+/);
169
+ if (parts.length !== 2 || parts.some((part) => !identifiers.has(part))) {
170
+ invalidMappingReference(interaction, declaration, entry, diagnostics);
171
+ }
172
+ }
173
+ }
174
+ function invalidMappingReference(interaction, declaration, entry, diagnostics) {
175
+ diagnostics.push({
176
+ code: "mapping.mapKey.reference",
177
+ severity: "error",
178
+ message: `Response declaration ${declaration.identifier} map-key ${entry.mapKey ?? ""} does not reference choices in ${interaction.qtiName}.`,
179
+ path: entry.source?.path ?? declaration.source?.path,
180
+ source: entry.source ?? declaration.source,
181
+ });
182
+ }
183
+ function validateInteractionChildren(interaction, diagnostics) {
184
+ const allowed = allowedInteractionChildren(interaction);
185
+ if (!allowed)
186
+ return;
187
+ for (const child of interaction.childElements) {
188
+ if (allowed.has(child.qtiName))
189
+ continue;
190
+ diagnostics.push({
191
+ code: "interaction.child.unsupported",
192
+ severity: "error",
193
+ message: `${interaction.qtiName} does not allow ${child.qtiName} as a direct child.`,
194
+ path: child.source?.path ?? interaction.source?.path,
195
+ source: child.source ?? interaction.source,
196
+ });
197
+ }
198
+ }
199
+ function validateInteractionRequiredAttributes(interaction, diagnostics) {
200
+ if (requiresObject(interaction) && !hasRequiredObjectAsset(interaction)) {
201
+ diagnostics.push({
202
+ code: "interaction.object.required",
203
+ severity: "error",
204
+ message: interaction.type === "drawing"
205
+ ? `${interaction.qtiName} requires an object, img, or picture canvas with a data/src attribute.`
206
+ : `${interaction.qtiName} requires an object, img, audio, or video child with a data/src attribute or media sources.`,
207
+ path: interaction.source?.path,
208
+ source: interaction.source,
209
+ });
210
+ }
211
+ if (interaction.type === "portableCustom") {
212
+ requireInteractionAttribute(interaction, "custom-interaction-type-identifier", "interaction.portableCustom.typeIdentifier", diagnostics);
213
+ }
214
+ if (interaction.type === "slider") {
215
+ const lower = interaction.attributes["lower-bound"];
216
+ const upper = interaction.attributes["upper-bound"];
217
+ requireInteractionAttribute(interaction, "lower-bound", "interaction.slider.lowerBound", diagnostics);
218
+ requireInteractionAttribute(interaction, "upper-bound", "interaction.slider.upperBound", diagnostics);
219
+ if (lower !== undefined && !isFiniteNumber(lower)) {
220
+ invalidNumber(interaction, "lower-bound", lower, diagnostics);
221
+ }
222
+ if (upper !== undefined && !isFiniteNumber(upper)) {
223
+ invalidNumber(interaction, "upper-bound", upper, diagnostics);
224
+ }
225
+ if (lower !== undefined &&
226
+ upper !== undefined &&
227
+ isFiniteNumber(lower) &&
228
+ isFiniteNumber(upper)) {
229
+ if (Number(lower) >= Number(upper)) {
230
+ diagnostics.push({
231
+ code: "interaction.slider.bounds",
232
+ severity: "error",
233
+ message: `${interaction.qtiName} requires lower-bound to be less than upper-bound.`,
234
+ path: interaction.source?.path,
235
+ source: interaction.source,
236
+ });
237
+ }
238
+ }
239
+ const step = interaction.attributes.step;
240
+ if (step !== undefined && (!isFiniteNumber(step) || Number(step) <= 0)) {
241
+ invalidNumber(interaction, "step", step, diagnostics);
242
+ }
243
+ }
244
+ }
245
+ function requiresObject(interaction) {
246
+ return (interaction.type === "graphicOrder" ||
247
+ interaction.type === "graphicAssociate" ||
248
+ interaction.type === "graphicGapMatch" ||
249
+ interaction.type === "hotspot" ||
250
+ interaction.type === "selectPoint" ||
251
+ interaction.type === "positionObject" ||
252
+ interaction.type === "media" ||
253
+ interaction.type === "drawing");
254
+ }
255
+ function hasRequiredObjectAsset(interaction) {
256
+ if (interaction.type === "media") {
257
+ return Boolean(interaction.object?.data || interaction.object?.sources.some((source) => Boolean(source.src)));
258
+ }
259
+ if (interaction.type === "drawing")
260
+ return Boolean(interaction.object?.data);
261
+ return Boolean(interaction.object?.data);
262
+ }
263
+ function requireInteractionAttribute(interaction, attribute, code, diagnostics) {
264
+ if (interaction.attributes[attribute])
265
+ return;
266
+ diagnostics.push({
267
+ code,
268
+ severity: "error",
269
+ message: `${interaction.qtiName} requires ${attribute}.`,
270
+ path: interaction.source?.path,
271
+ source: interaction.source,
272
+ });
273
+ }
274
+ function validatePortableCustomInteraction(interaction, item, diagnostics) {
275
+ if (interaction.type !== "portableCustom")
276
+ return;
277
+ const definition = interaction.portableCustom;
278
+ if (!definition)
279
+ return;
280
+ const configuredModules = definition.interactionModules?.modules ?? [];
281
+ const hasModuleAttribute = Boolean(definition.module?.trim());
282
+ const hasConfiguredModule = configuredModules.some((module) => Boolean(module.id?.trim()));
283
+ if (!hasModuleAttribute && !hasConfiguredModule) {
284
+ diagnostics.push({
285
+ code: "interaction.portableCustom.module",
286
+ severity: "error",
287
+ message: `${interaction.qtiName} requires a module attribute or at least one qti-interaction-module id.`,
288
+ path: interaction.source?.path,
289
+ source: interaction.source,
290
+ });
291
+ }
292
+ for (const module of configuredModules) {
293
+ if (!module.id?.trim()) {
294
+ diagnostics.push({
295
+ code: "interaction.portableCustom.moduleId",
296
+ severity: "error",
297
+ message: "qti-interaction-module requires a non-empty id.",
298
+ path: module.source?.path,
299
+ source: module.source,
300
+ });
301
+ }
302
+ warnExternalPortableCustomUrl(module.primaryPath, module.source, diagnostics);
303
+ warnExternalPortableCustomUrl(module.fallbackPath, module.source, diagnostics);
304
+ }
305
+ warnExternalPortableCustomUrl(definition.interactionModules?.primaryConfiguration, definition.interactionModules?.source, diagnostics);
306
+ warnExternalPortableCustomUrl(definition.interactionModules?.secondaryConfiguration, definition.interactionModules?.source, diagnostics);
307
+ const templateIdentifiers = new Set(item.templateDeclarations.map((declaration) => declaration.identifier));
308
+ for (const variable of definition.templateVariables) {
309
+ if (!variable.identifier?.trim()) {
310
+ diagnostics.push({
311
+ code: "interaction.portableCustom.templateVariable",
312
+ severity: "error",
313
+ message: "qti-template-variable requires template-identifier or identifier.",
314
+ path: variable.source?.path,
315
+ source: variable.source,
316
+ });
317
+ continue;
318
+ }
319
+ if (!templateIdentifiers.has(variable.identifier)) {
320
+ diagnostics.push({
321
+ code: "interaction.portableCustom.templateVariable.reference",
322
+ severity: "error",
323
+ message: `qti-template-variable references missing template declaration ${variable.identifier}.`,
324
+ path: variable.source?.path,
325
+ source: variable.source,
326
+ });
327
+ }
328
+ }
329
+ for (const variable of definition.contextVariables) {
330
+ if (variable.identifier?.trim())
331
+ continue;
332
+ diagnostics.push({
333
+ code: "interaction.portableCustom.contextVariable",
334
+ severity: "error",
335
+ message: "qti-context-variable requires identifier.",
336
+ path: variable.source?.path,
337
+ source: variable.source,
338
+ });
339
+ }
340
+ for (const stylesheet of definition.stylesheets) {
341
+ if (stylesheet.href.trim().length > 0)
342
+ continue;
343
+ diagnostics.push({
344
+ code: "stylesheet.href.required",
345
+ severity: "error",
346
+ message: "qti-stylesheet requires a non-empty href attribute.",
347
+ path: stylesheet.source?.path,
348
+ source: stylesheet.source,
349
+ });
350
+ }
351
+ }
352
+ function warnExternalPortableCustomUrl(url, source, diagnostics) {
353
+ if (!url || !/^https?:\/\//i.test(url))
354
+ return;
355
+ diagnostics.push({
356
+ code: "interaction.portableCustom.externalModuleUrl",
357
+ severity: "warning",
358
+ message: `Portable custom interaction module URL ${url} requires host delivery policy approval.`,
359
+ path: source?.path,
360
+ source,
361
+ });
362
+ }
363
+ function invalidNumber(interaction, attribute, value, diagnostics) {
364
+ diagnostics.push({
365
+ code: "interaction.numericAttribute",
366
+ severity: "error",
367
+ message: `${interaction.qtiName} requires numeric ${attribute}, got ${value}.`,
368
+ path: interaction.source?.path,
369
+ source: interaction.source,
370
+ });
371
+ }
372
+ function validatePatternMaskAttribute(interaction, diagnostics) {
373
+ if (interaction.type !== "textEntry" && interaction.type !== "extendedText")
374
+ return;
375
+ const patternMask = interaction.attributes["pattern-mask"];
376
+ if (patternMask === undefined || isValidQtiPatternMask(patternMask))
377
+ return;
378
+ diagnostics.push({
379
+ code: "interaction.patternMask.invalid",
380
+ severity: "error",
381
+ message: `${interaction.qtiName} pattern-mask is not a valid regular expression.`,
382
+ path: interaction.source?.path,
383
+ source: interaction.source,
384
+ });
385
+ }
386
+ function validateInteractionLimitAttributes(interaction, diagnostics) {
387
+ validateNonNegativeIntegerAttribute(interaction, "max-choices", diagnostics);
388
+ validateNonNegativeIntegerAttribute(interaction, "min-choices", diagnostics);
389
+ validateNonNegativeIntegerAttribute(interaction, "max-associations", diagnostics);
390
+ validateNonNegativeIntegerAttribute(interaction, "min-associations", diagnostics);
391
+ validateNonNegativeIntegerAttribute(interaction, "expected-length", diagnostics);
392
+ validateNonNegativeIntegerAttribute(interaction, "expected-lines", diagnostics);
393
+ validateMinMaxPair(interaction, "min-choices", "max-choices", diagnostics);
394
+ validateMinMaxPair(interaction, "min-associations", "max-associations", diagnostics);
395
+ if (interaction.type === "media") {
396
+ validateNonNegativeIntegerAttribute(interaction, "max-plays", diagnostics);
397
+ validateNonNegativeIntegerAttribute(interaction, "min-plays", diagnostics);
398
+ validateBooleanAttribute(interaction, "autostart", diagnostics);
399
+ validateBooleanAttribute(interaction, "loop", diagnostics);
400
+ validateMinMaxPair(interaction, "min-plays", "max-plays", diagnostics);
401
+ }
402
+ }
403
+ function validateChoiceLimitAttributes(choice, diagnostics) {
404
+ if (requiresMatchMax(choice) && !choice.attributes["match-max"]) {
405
+ diagnostics.push({
406
+ code: "choice.matchMax.required",
407
+ severity: "error",
408
+ message: `${choice.qtiName} ${choice.identifier} requires match-max.`,
409
+ path: choice.source?.path,
410
+ source: choice.source,
411
+ });
412
+ }
413
+ validateChoiceNonNegativeIntegerAttribute(choice, "match-max", diagnostics);
414
+ validateChoiceNonNegativeIntegerAttribute(choice, "match-min", diagnostics);
415
+ validateChoiceMinMaxPair(choice, "match-min", "match-max", diagnostics);
416
+ validateGapImageAsset(choice, diagnostics);
417
+ validateHotspotGeometry(choice, diagnostics);
418
+ }
419
+ function validateGapImageAsset(choice, diagnostics) {
420
+ if (choice.qtiName !== "qti-gap-img" || choice.asset?.data)
421
+ return;
422
+ diagnostics.push({
423
+ code: "choice.gapImg.media.required",
424
+ severity: "error",
425
+ message: `qti-gap-img ${choice.identifier} requires an img, object, or picture child with a usable src or data attribute.`,
426
+ path: choice.source?.path,
427
+ source: choice.source,
428
+ });
429
+ }
430
+ function requiresMatchMax(choice) {
431
+ return (choice.qtiName === "qti-simple-associable-choice" ||
432
+ choice.qtiName === "qti-associable-hotspot" ||
433
+ choice.qtiName === "qti-gap-text" ||
434
+ choice.qtiName === "qti-gap-img");
435
+ }
436
+ function validateNonNegativeIntegerAttribute(interaction, attribute, diagnostics) {
437
+ const value = interaction.attributes[attribute];
438
+ if (value === undefined || isNonNegativeInteger(value))
439
+ return;
440
+ diagnostics.push({
441
+ code: "interaction.integerAttribute",
442
+ severity: "error",
443
+ message: `${interaction.qtiName} requires non-negative integer ${attribute}, got ${value}.`,
444
+ path: interaction.source?.path,
445
+ source: interaction.source,
446
+ });
447
+ }
448
+ function validateBooleanAttribute(interaction, attribute, diagnostics) {
449
+ const value = interaction.attributes[attribute];
450
+ if (value === undefined || isBooleanAttribute(value))
451
+ return;
452
+ diagnostics.push({
453
+ code: "interaction.booleanAttribute",
454
+ severity: "error",
455
+ message: `${interaction.qtiName} requires boolean ${attribute}, got ${value}.`,
456
+ path: interaction.source?.path,
457
+ source: interaction.source,
458
+ });
459
+ }
460
+ function validateChoiceNonNegativeIntegerAttribute(choice, attribute, diagnostics) {
461
+ const value = choice.attributes[attribute];
462
+ if (value === undefined || isNonNegativeInteger(value))
463
+ return;
464
+ diagnostics.push({
465
+ code: "choice.integerAttribute",
466
+ severity: "error",
467
+ message: `${choice.qtiName} ${choice.identifier} requires non-negative integer ${attribute}, got ${value}.`,
468
+ path: choice.source?.path,
469
+ source: choice.source,
470
+ });
471
+ }
472
+ function validateChoiceMinMaxPair(choice, minAttribute, maxAttribute, diagnostics) {
473
+ const min = choice.attributes[minAttribute];
474
+ const max = choice.attributes[maxAttribute];
475
+ if (min === undefined ||
476
+ max === undefined ||
477
+ !isNonNegativeInteger(min) ||
478
+ !isNonNegativeInteger(max) ||
479
+ max === "0") {
480
+ return;
481
+ }
482
+ if (Number(min) <= Number(max))
483
+ return;
484
+ diagnostics.push({
485
+ code: "choice.minMax",
486
+ severity: "error",
487
+ message: `${choice.qtiName} ${choice.identifier} requires ${minAttribute} to be less than or equal to ${maxAttribute}, unless ${maxAttribute} is 0 for unlimited.`,
488
+ path: choice.source?.path,
489
+ source: choice.source,
490
+ });
491
+ }
492
+ function validateMinMaxPair(interaction, minAttribute, maxAttribute, diagnostics) {
493
+ const min = interaction.attributes[minAttribute];
494
+ const max = interaction.attributes[maxAttribute];
495
+ if (min === undefined ||
496
+ max === undefined ||
497
+ !isNonNegativeInteger(min) ||
498
+ !isNonNegativeInteger(max) ||
499
+ max === "0") {
500
+ return;
501
+ }
502
+ if (Number(min) <= Number(max))
503
+ return;
504
+ diagnostics.push({
505
+ code: "interaction.minMax",
506
+ severity: "error",
507
+ message: `${interaction.qtiName} requires ${minAttribute} to be less than or equal to ${maxAttribute}, unless ${maxAttribute} is 0 for unlimited.`,
508
+ path: interaction.source?.path,
509
+ source: interaction.source,
510
+ });
511
+ }
512
+ function allowedInteractionChildren(interaction) {
513
+ const common = ["qti-prompt"];
514
+ switch (interaction.type) {
515
+ case "choice":
516
+ return setOf(common, ["qti-simple-choice"]);
517
+ case "order":
518
+ return setOf(common, ["qti-simple-choice"]);
519
+ case "associate":
520
+ return setOf(common, ["qti-simple-match-set", "qti-simple-associable-choice"]);
521
+ case "match":
522
+ return setOf(common, ["qti-simple-match-set"]);
523
+ case "gapMatch":
524
+ return setOf(common, ["qti-gap-text", "qti-gap-img", ...staticContentNames()]);
525
+ case "inlineChoice":
526
+ return setOf(common, ["qti-inline-choice"]);
527
+ case "hottext":
528
+ return setOf(common, staticContentNames());
529
+ case "graphicOrder":
530
+ return setOf(common, ["object", "qti-hotspot-choice"]);
531
+ case "graphicAssociate":
532
+ return setOf(common, ["object", "qti-associable-hotspot"]);
533
+ case "graphicGapMatch":
534
+ return setOf(common, [
535
+ "object",
536
+ "qti-gap-text",
537
+ "qti-gap-img",
538
+ "qti-associable-hotspot",
539
+ ...staticContentNames(),
540
+ ]);
541
+ case "hotspot":
542
+ return setOf(common, ["object", "qti-hotspot-choice"]);
543
+ case "positionObject":
544
+ return setOf(common, ["object", "img", "qti-position-object-stage"]);
545
+ case "selectPoint":
546
+ case "media":
547
+ return setOf(common, ["audio", "video", "object", "img"]);
548
+ case "drawing":
549
+ return setOf(common, ["object", "img", "picture"]);
550
+ case "extendedText":
551
+ return new Set(common);
552
+ case "portableCustom":
553
+ return setOf(common, [
554
+ "qti-interaction-markup",
555
+ "qti-interaction-modules",
556
+ "qti-template-variable",
557
+ "qti-context-variable",
558
+ "qti-stylesheet",
559
+ "qti-catalog-info",
560
+ ]);
561
+ case "slider":
562
+ case "textEntry":
563
+ case "upload":
564
+ case "endAttempt":
565
+ return new Set(common);
566
+ case "custom":
567
+ return undefined;
568
+ default:
569
+ return assertNever(interaction.type);
570
+ }
571
+ }
572
+ function staticContentNames() {
573
+ return ["p", "div", "span", "ul", "ol", "li", "table", "tbody", "thead", "tr", "td", "th"];
574
+ }
575
+ function setOf(...items) {
576
+ return new Set(items.flat());
577
+ }
578
+ function expectedResponseShape(interaction) {
579
+ if (interaction.type === "endAttempt") {
580
+ return { cardinalities: ["single"], baseTypes: ["boolean"] };
581
+ }
582
+ if (interaction.type === "media")
583
+ return { cardinalities: ["single"], baseTypes: ["integer"] };
584
+ if (interaction.type === "custom")
585
+ return undefined;
586
+ if (interaction.type === "order" || interaction.type === "graphicOrder") {
587
+ return { cardinalities: ["ordered"], baseTypes: ["identifier"] };
588
+ }
589
+ if (interaction.type === "associate" || interaction.type === "graphicAssociate") {
590
+ return { cardinalities: ["multiple"], baseTypes: ["pair", "directedPair"] };
591
+ }
592
+ if (interaction.type === "match" ||
593
+ interaction.type === "gapMatch" ||
594
+ interaction.type === "graphicGapMatch") {
595
+ return { cardinalities: ["multiple"], baseTypes: ["directedPair"] };
596
+ }
597
+ if (interaction.type === "selectPoint" || interaction.type === "positionObject") {
598
+ return { cardinalities: ["single", "multiple"], baseTypes: ["point"] };
599
+ }
600
+ if (interaction.type === "slider") {
601
+ return { cardinalities: ["single"], baseTypes: ["integer", "float"] };
602
+ }
603
+ if (interaction.type === "upload") {
604
+ return { cardinalities: ["single"], baseTypes: ["file"] };
605
+ }
606
+ if (interaction.type === "textEntry" || interaction.type === "extendedText") {
607
+ return { cardinalities: ["single"], baseTypes: ["string"] };
608
+ }
609
+ if (interaction.type === "drawing")
610
+ return { cardinalities: ["single"], baseTypes: ["file"] };
611
+ if (interaction.type === "portableCustom") {
612
+ return {
613
+ cardinalities: ["single", "multiple", "ordered", "record"],
614
+ baseTypes: [
615
+ "identifier",
616
+ "boolean",
617
+ "integer",
618
+ "float",
619
+ "string",
620
+ "point",
621
+ "pair",
622
+ "directedPair",
623
+ "duration",
624
+ "file",
625
+ "uri",
626
+ ],
627
+ };
628
+ }
629
+ return { cardinalities: ["single", "multiple"], baseTypes: ["identifier"] };
630
+ }
631
+ function needsChoices(interaction) {
632
+ return (interaction.type === "choice" ||
633
+ interaction.type === "order" ||
634
+ interaction.type === "associate" ||
635
+ interaction.type === "match" ||
636
+ interaction.type === "gapMatch" ||
637
+ interaction.type === "inlineChoice" ||
638
+ interaction.type === "hottext" ||
639
+ interaction.type === "graphicOrder" ||
640
+ interaction.type === "graphicAssociate" ||
641
+ interaction.type === "graphicGapMatch" ||
642
+ interaction.type === "hotspot");
643
+ }
644
+ //# sourceMappingURL=validation-interactions.js.map