@longsightgroup/qti3-core 0.10.6 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/README.md +126 -0
  2. package/dist/adaptive-turn-materializer.js +4 -5
  3. package/dist/adaptive-turn-materializer.js.map +1 -1
  4. package/dist/adaptive-turn.d.ts.map +1 -1
  5. package/dist/adaptive-turn.js +1 -0
  6. package/dist/adaptive-turn.js.map +1 -1
  7. package/dist/attempt-state.d.ts.map +1 -1
  8. package/dist/attempt-state.js +41 -1
  9. package/dist/attempt-state.js.map +1 -1
  10. package/dist/binary-choice.d.ts +9 -0
  11. package/dist/binary-choice.d.ts.map +1 -0
  12. package/dist/binary-choice.js +81 -0
  13. package/dist/binary-choice.js.map +1 -0
  14. package/dist/diagnostics.d.ts +13 -0
  15. package/dist/diagnostics.d.ts.map +1 -0
  16. package/dist/diagnostics.js +28 -0
  17. package/dist/diagnostics.js.map +1 -0
  18. package/dist/index.d.ts +19 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +12 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/item-submission-materialization.d.ts.map +1 -1
  23. package/dist/item-submission-materialization.js +2 -0
  24. package/dist/item-submission-materialization.js.map +1 -1
  25. package/dist/parser-processing.d.ts.map +1 -1
  26. package/dist/parser-processing.js +8 -18
  27. package/dist/parser-processing.js.map +1 -1
  28. package/dist/parser-values.d.ts.map +1 -1
  29. package/dist/parser-values.js +1 -3
  30. package/dist/parser-values.js.map +1 -1
  31. package/dist/parser.js +32 -0
  32. package/dist/parser.js.map +1 -1
  33. package/dist/processing-equality.d.ts +10 -0
  34. package/dist/processing-equality.d.ts.map +1 -0
  35. package/dist/processing-equality.js +78 -0
  36. package/dist/processing-equality.js.map +1 -0
  37. package/dist/processing-evaluator-comparison.d.ts.map +1 -1
  38. package/dist/processing-evaluator-comparison.js +14 -9
  39. package/dist/processing-evaluator-comparison.js.map +1 -1
  40. package/dist/processing-evaluator-numeric.d.ts.map +1 -1
  41. package/dist/processing-evaluator-numeric.js +2 -1
  42. package/dist/processing-evaluator-numeric.js.map +1 -1
  43. package/dist/processing-evaluator-string.d.ts.map +1 -1
  44. package/dist/processing-evaluator-string.js +26 -10
  45. package/dist/processing-evaluator-string.js.map +1 -1
  46. package/dist/processing-evaluator-variable.d.ts.map +1 -1
  47. package/dist/processing-evaluator-variable.js +9 -3
  48. package/dist/processing-evaluator-variable.js.map +1 -1
  49. package/dist/processing-evaluator.d.ts +5 -1
  50. package/dist/processing-evaluator.d.ts.map +1 -1
  51. package/dist/processing-evaluator.js +13 -4
  52. package/dist/processing-evaluator.js.map +1 -1
  53. package/dist/processing-expression-children.d.ts.map +1 -1
  54. package/dist/processing-expression-children.js +2 -3
  55. package/dist/processing-expression-children.js.map +1 -1
  56. package/dist/processing-expression-tags.d.ts +2 -1
  57. package/dist/processing-expression-tags.d.ts.map +1 -1
  58. package/dist/processing-expression-tags.js +2 -1
  59. package/dist/processing-expression-tags.js.map +1 -1
  60. package/dist/processing-mapping.d.ts.map +1 -1
  61. package/dist/processing-mapping.js +26 -16
  62. package/dist/processing-mapping.js.map +1 -1
  63. package/dist/processing-state.d.ts +2 -2
  64. package/dist/processing-state.d.ts.map +1 -1
  65. package/dist/processing-state.js +5 -2
  66. package/dist/processing-state.js.map +1 -1
  67. package/dist/processing-values.d.ts +1 -2
  68. package/dist/processing-values.d.ts.map +1 -1
  69. package/dist/processing-values.js +1 -3
  70. package/dist/processing-values.js.map +1 -1
  71. package/dist/processing-variables.d.ts.map +1 -1
  72. package/dist/processing-variables.js +10 -5
  73. package/dist/processing-variables.js.map +1 -1
  74. package/dist/qti-identifier.d.ts +3 -0
  75. package/dist/qti-identifier.d.ts.map +1 -0
  76. package/dist/qti-identifier.js +5 -0
  77. package/dist/qti-identifier.js.map +1 -0
  78. package/dist/qti-package-assets.d.ts.map +1 -1
  79. package/dist/qti-package-assets.js +16 -10
  80. package/dist/qti-package-assets.js.map +1 -1
  81. package/dist/qti-package-manifest.d.ts +1 -0
  82. package/dist/qti-package-manifest.d.ts.map +1 -1
  83. package/dist/qti-package-manifest.js +1 -0
  84. package/dist/qti-package-manifest.js.map +1 -1
  85. package/dist/qti-package-metadata.d.ts.map +1 -1
  86. package/dist/qti-package-metadata.js +11 -0
  87. package/dist/qti-package-metadata.js.map +1 -1
  88. package/dist/qti-package-paths.d.ts +2 -0
  89. package/dist/qti-package-paths.d.ts.map +1 -1
  90. package/dist/qti-package-paths.js +6 -0
  91. package/dist/qti-package-paths.js.map +1 -1
  92. package/dist/qti-package-plan.d.ts +1 -1
  93. package/dist/qti-package-plan.d.ts.map +1 -1
  94. package/dist/qti-package-types.d.ts +9 -0
  95. package/dist/qti-package-types.d.ts.map +1 -1
  96. package/dist/qti-package-xml.d.ts +6 -1
  97. package/dist/qti-package-xml.d.ts.map +1 -1
  98. package/dist/qti-package-xml.js +11 -6
  99. package/dist/qti-package-xml.js.map +1 -1
  100. package/dist/qti-package-zip.d.ts +13 -1
  101. package/dist/qti-package-zip.d.ts.map +1 -1
  102. package/dist/qti-package-zip.js +185 -54
  103. package/dist/qti-package-zip.js.map +1 -1
  104. package/dist/qti-package.d.ts +12 -5
  105. package/dist/qti-package.d.ts.map +1 -1
  106. package/dist/qti-package.js +63 -8
  107. package/dist/qti-package.js.map +1 -1
  108. package/dist/response-validation-policy.d.ts.map +1 -1
  109. package/dist/response-validation-policy.js +3 -1
  110. package/dist/response-validation-policy.js.map +1 -1
  111. package/dist/response-validation.d.ts.map +1 -1
  112. package/dist/response-validation.js +2 -1
  113. package/dist/response-validation.js.map +1 -1
  114. package/dist/serializer-processing-expressions.d.ts.map +1 -1
  115. package/dist/serializer-processing-expressions.js +2 -10
  116. package/dist/serializer-processing-expressions.js.map +1 -1
  117. package/dist/server-scoring.d.ts.map +1 -1
  118. package/dist/server-scoring.js +1 -0
  119. package/dist/server-scoring.js.map +1 -1
  120. package/dist/session-builtins.d.ts +16 -0
  121. package/dist/session-builtins.d.ts.map +1 -0
  122. package/dist/session-builtins.js +58 -0
  123. package/dist/session-builtins.js.map +1 -0
  124. package/dist/session.d.ts +5 -1
  125. package/dist/session.d.ts.map +1 -1
  126. package/dist/session.js +63 -11
  127. package/dist/session.js.map +1 -1
  128. package/dist/support-evidence.d.ts.map +1 -1
  129. package/dist/support-evidence.js +2 -0
  130. package/dist/support-evidence.js.map +1 -1
  131. package/dist/support.d.ts.map +1 -1
  132. package/dist/support.js +66 -23
  133. package/dist/support.js.map +1 -1
  134. package/dist/test-expression-parser.d.ts +6 -0
  135. package/dist/test-expression-parser.d.ts.map +1 -0
  136. package/dist/test-expression-parser.js +76 -0
  137. package/dist/test-expression-parser.js.map +1 -0
  138. package/dist/test-expression-serializer.d.ts +5 -0
  139. package/dist/test-expression-serializer.d.ts.map +1 -0
  140. package/dist/test-expression-serializer.js +54 -0
  141. package/dist/test-expression-serializer.js.map +1 -0
  142. package/dist/test-expression-validation.d.ts +14 -0
  143. package/dist/test-expression-validation.d.ts.map +1 -0
  144. package/dist/test-expression-validation.js +101 -0
  145. package/dist/test-expression-validation.js.map +1 -0
  146. package/dist/test-expression.d.ts +99 -0
  147. package/dist/test-expression.d.ts.map +1 -0
  148. package/dist/test-expression.js +28 -0
  149. package/dist/test-expression.js.map +1 -0
  150. package/dist/test-model.d.ts +62 -0
  151. package/dist/test-model.d.ts.map +1 -0
  152. package/dist/test-model.js +5 -0
  153. package/dist/test-model.js.map +1 -0
  154. package/dist/test-parser.d.ts +13 -0
  155. package/dist/test-parser.d.ts.map +1 -0
  156. package/dist/test-parser.js +144 -0
  157. package/dist/test-parser.js.map +1 -0
  158. package/dist/test-processing.d.ts +22 -0
  159. package/dist/test-processing.d.ts.map +1 -0
  160. package/dist/test-processing.js +84 -0
  161. package/dist/test-processing.js.map +1 -0
  162. package/dist/test-session.d.ts +31 -0
  163. package/dist/test-session.d.ts.map +1 -0
  164. package/dist/test-session.js +102 -0
  165. package/dist/test-session.js.map +1 -0
  166. package/dist/test-support.d.ts +4 -0
  167. package/dist/test-support.d.ts.map +1 -0
  168. package/dist/test-support.js +30 -0
  169. package/dist/test-support.js.map +1 -0
  170. package/dist/test-validation.d.ts +4 -0
  171. package/dist/test-validation.d.ts.map +1 -0
  172. package/dist/test-validation.js +133 -0
  173. package/dist/test-validation.js.map +1 -0
  174. package/dist/test-xml.d.ts +7 -0
  175. package/dist/test-xml.d.ts.map +1 -0
  176. package/dist/test-xml.js +23 -0
  177. package/dist/test-xml.js.map +1 -0
  178. package/dist/trusted-item-session.d.ts +4 -1
  179. package/dist/trusted-item-session.d.ts.map +1 -1
  180. package/dist/trusted-item-session.js +8 -3
  181. package/dist/trusted-item-session.js.map +1 -1
  182. package/dist/types.d.ts +26 -4
  183. package/dist/types.d.ts.map +1 -1
  184. package/dist/validation-declaration-entries.d.ts.map +1 -1
  185. package/dist/validation-declaration-entries.js +11 -1
  186. package/dist/validation-declaration-entries.js.map +1 -1
  187. package/dist/validation-item-children.d.ts +4 -0
  188. package/dist/validation-item-children.d.ts.map +1 -0
  189. package/dist/validation-item-children.js +45 -0
  190. package/dist/validation-item-children.js.map +1 -0
  191. package/dist/validation-processing.d.ts.map +1 -1
  192. package/dist/validation-processing.js +45 -12
  193. package/dist/validation-processing.js.map +1 -1
  194. package/dist/validation.d.ts.map +1 -1
  195. package/dist/validation.js +12 -0
  196. package/dist/validation.js.map +1 -1
  197. package/dist/value-guards.d.ts +4 -0
  198. package/dist/value-guards.d.ts.map +1 -0
  199. package/dist/value-guards.js +5 -0
  200. package/dist/value-guards.js.map +1 -0
  201. package/dist/xml-schema-regex-data.d.ts +48 -0
  202. package/dist/xml-schema-regex-data.d.ts.map +1 -0
  203. package/dist/xml-schema-regex-data.js +403 -0
  204. package/dist/xml-schema-regex-data.js.map +1 -0
  205. package/dist/xml-schema-regex-parser.d.ts +31 -0
  206. package/dist/xml-schema-regex-parser.d.ts.map +1 -0
  207. package/dist/xml-schema-regex-parser.js +245 -0
  208. package/dist/xml-schema-regex-parser.js.map +1 -0
  209. package/dist/xml-schema-regex.d.ts +14 -0
  210. package/dist/xml-schema-regex.d.ts.map +1 -0
  211. package/dist/xml-schema-regex.js +114 -0
  212. package/dist/xml-schema-regex.js.map +1 -0
  213. package/package.json +1 -1
  214. package/src/adaptive-turn-materializer.ts +4 -3
  215. package/src/adaptive-turn.ts +1 -0
  216. package/src/attempt-state.ts +54 -1
  217. package/src/binary-choice.ts +96 -0
  218. package/src/diagnostics.ts +29 -0
  219. package/src/index.ts +38 -0
  220. package/src/item-submission-materialization.ts +2 -0
  221. package/src/parser-processing.ts +15 -18
  222. package/src/parser-values.ts +1 -5
  223. package/src/parser.ts +34 -0
  224. package/src/processing-equality.ts +104 -0
  225. package/src/processing-evaluator-comparison.ts +19 -14
  226. package/src/processing-evaluator-numeric.ts +2 -6
  227. package/src/processing-evaluator-string.ts +25 -18
  228. package/src/processing-evaluator-variable.ts +8 -3
  229. package/src/processing-evaluator.ts +20 -7
  230. package/src/processing-expression-children.ts +2 -3
  231. package/src/processing-expression-tags.ts +2 -1
  232. package/src/processing-mapping.ts +31 -25
  233. package/src/processing-state.ts +6 -1
  234. package/src/processing-values.ts +2 -11
  235. package/src/processing-variables.ts +9 -7
  236. package/src/qti-identifier.ts +4 -0
  237. package/src/qti-package-assets.ts +20 -13
  238. package/src/qti-package-manifest.ts +1 -0
  239. package/src/qti-package-metadata.ts +11 -0
  240. package/src/qti-package-paths.ts +9 -0
  241. package/src/qti-package-plan.ts +1 -1
  242. package/src/qti-package-types.ts +10 -0
  243. package/src/qti-package-xml.ts +11 -6
  244. package/src/qti-package-zip.ts +330 -97
  245. package/src/qti-package.ts +106 -7
  246. package/src/response-validation-policy.ts +5 -1
  247. package/src/response-validation.ts +2 -1
  248. package/src/serializer-processing-expressions.ts +8 -25
  249. package/src/server-scoring.ts +1 -0
  250. package/src/session-builtins.ts +75 -0
  251. package/src/session.ts +64 -8
  252. package/src/support-evidence.ts +2 -0
  253. package/src/support.ts +86 -23
  254. package/src/test-expression-parser.ts +89 -0
  255. package/src/test-expression-serializer.ts +71 -0
  256. package/src/test-expression-validation.ts +139 -0
  257. package/src/test-expression.ts +66 -0
  258. package/src/test-model.ts +68 -0
  259. package/src/test-parser.ts +201 -0
  260. package/src/test-processing.ts +107 -0
  261. package/src/test-session.ts +160 -0
  262. package/src/test-support.ts +31 -0
  263. package/src/test-validation.ts +190 -0
  264. package/src/test-xml.ts +37 -0
  265. package/src/trusted-item-session.ts +13 -2
  266. package/src/types.ts +36 -4
  267. package/src/validation-declaration-entries.ts +11 -1
  268. package/src/validation-item-children.ts +48 -0
  269. package/src/validation-processing.ts +47 -10
  270. package/src/validation.ts +14 -0
  271. package/src/value-guards.ts +6 -0
  272. package/src/xml-schema-regex-data.ts +404 -0
  273. package/src/xml-schema-regex-parser.ts +234 -0
  274. package/src/xml-schema-regex.ts +115 -0
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export {
5
5
  type QtiAdaptiveTurnResult,
6
6
  } from "./adaptive-turn.js";
7
7
  export { assertNever } from "./assert-never.js";
8
+ export { diagnosticKey, uniqueDiagnostics } from "./diagnostics.js";
8
9
  export {
9
10
  createCatalogSupportResolution,
10
11
  type QtiCatalogSupportResolution,
@@ -57,9 +58,13 @@ export {
57
58
  discoverQtiPackageContentAssets,
58
59
  normalizePackagePath,
59
60
  QTI_ITEM_RESOURCE_TYPE,
61
+ isQtiItemResource,
60
62
  QTI_PACKAGE_MANIFEST_PATH,
61
63
  parseQtiPackage,
64
+ parseQtiPackageFromEntries,
62
65
  readQtiPackageZipEntries,
66
+ readQtiPackageZipEntriesAsync,
67
+ scopeDiagnosticToPackagePath,
63
68
  type QtiAssessmentSectionPackageModel,
64
69
  type QtiAssessmentTestItemRef,
65
70
  type QtiAssessmentTestPackageModel,
@@ -71,11 +76,14 @@ export {
71
76
  type QtiPackageEntry,
72
77
  type QtiPackageInflateContext,
73
78
  type QtiPackageInflateRaw,
79
+ type QtiPackageInflateRawAsync,
80
+ type QtiPackageZipAsyncOptions,
74
81
  type QtiItemSessionControl,
75
82
  type QtiPackageItem,
76
83
  type QtiPackageItemSource,
77
84
  type QtiPackageParseOptions,
78
85
  type QtiPackageParseResult,
86
+ type QtiPackageXmlFileSummary,
79
87
  type QtiPackageResourceLimits,
80
88
  type QtiPackageShape,
81
89
  type QtiStandardAlignment,
@@ -184,6 +192,7 @@ export {
184
192
  type QtiCustomOperatorRegistry,
185
193
  type QtiItemSession,
186
194
  type QtiItemSessionOptions,
195
+ type QtiSessionEnvironment,
187
196
  } from "./session.js";
188
197
  export {
189
198
  deprecatedInteractionSupport,
@@ -383,3 +392,32 @@ export {
383
392
  formatQtiTextResponse,
384
393
  qtiTextResponseString,
385
394
  } from "./text-response.js";
395
+ export type {
396
+ QtiExecutableTest,
397
+ QtiTestDefinition,
398
+ QtiTestSection,
399
+ QtiTestItemRef,
400
+ QtiTestBranch,
401
+ QtiTestResult,
402
+ QtiTestOutcomeRule,
403
+ } from "./test-model.js";
404
+ export { validateQtiTest } from "./test-validation.js";
405
+ export { parseQtiTest } from "./test-parser.js";
406
+ export { parseQtiTestExecution } from "./test-parser.js";
407
+ export type { QtiTestExecution } from "./test-parser.js";
408
+ export {
409
+ startQtiTest,
410
+ submitQtiTestAnswer,
411
+ snapshotQtiTestSession,
412
+ restoreQtiTestSession,
413
+ } from "./test-session.js";
414
+ export type { QtiTestSession, QtiTestSessionSnapshot } from "./test-session.js";
415
+ export type { QtiTestSubmission } from "./test-processing.js";
416
+ export type { QtiTestExpression, QtiTestBaseType } from "./test-expression.js";
417
+ export { serializeTestExpression } from "./test-expression-serializer.js";
418
+ export { isQtiIdentifier } from "./qti-identifier.js";
419
+ export { isQtiPackageItemHref } from "./qti-package-paths.js";
420
+ export { testFailure } from "./test-model.js";
421
+ export { testExecutionSupport } from "./test-support.js";
422
+ export { inspectQtiBinaryChoice } from "./binary-choice.js";
423
+ export type { QtiBinaryChoicePolicy } from "./binary-choice.js";
@@ -65,6 +65,7 @@ export function materializeQtiItemSubmission(
65
65
  parsedItem: parsedResult.parsed,
66
66
  diagnosticPrefix: "itemSubmission",
67
67
  allowedUndeclaredResponseIdentifiers: input.allowedUndeclaredResponseIdentifiers,
68
+ sessionEnvironment: input.sessionEnvironment,
68
69
  priorState: priorState.state,
69
70
  submission: {
70
71
  trustedResponses: input.trustedResponses,
@@ -98,6 +99,7 @@ function materializeAdaptiveSubmission(
98
99
  parsedItem,
99
100
  diagnosticPrefix: "itemSubmission",
100
101
  allowedUndeclaredResponseIdentifiers: input.allowedUndeclaredResponseIdentifiers,
102
+ sessionEnvironment: input.sessionEnvironment,
101
103
  priorState: input.existingState,
102
104
  trustedResponses: input.trustedResponses,
103
105
  trustedInteractionStates: input.trustedInteractionStates,
@@ -1,4 +1,10 @@
1
- import { coerceValue, parseCoords, parseShape, parseXmlBoolean } from "./parser-values.js";
1
+ import {
2
+ coerceValue,
3
+ parseCoords,
4
+ parseInteger,
5
+ parseShape,
6
+ parseXmlBoolean,
7
+ } from "./parser-values.js";
2
8
  import { responseConditionsFromRules } from "./processing-rules.js";
3
9
  import type {
4
10
  QtiLookupOutcomeValue,
@@ -215,12 +221,8 @@ function parseExpression(node: XmlNode): QtiProcessingExpression | undefined {
215
221
  }
216
222
 
217
223
  if (node.localName === "qti-is-null") {
218
- const variable = childElements(node, "qti-variable")[0];
219
- return {
220
- type: "isNull",
221
- identifier: variable?.attributes.identifier ?? "",
222
- source: node.source,
223
- };
224
+ const expression = parseFirstExpression(node);
225
+ return expression ? { type: "isNull", expression, source: node.source } : undefined;
224
226
  }
225
227
 
226
228
  if (node.localName === "qti-map-response") {
@@ -281,14 +283,8 @@ function parseExpression(node: XmlNode): QtiProcessingExpression | undefined {
281
283
  }
282
284
 
283
285
  if (node.localName === "qti-random") {
284
- const multiple = childElements(node, "qti-multiple")[0];
285
- return {
286
- type: "random",
287
- values: childElements(multiple ?? node)
288
- .map(parseExpression)
289
- .filter((expression): expression is QtiProcessingExpression => expression !== undefined),
290
- source: node.source,
291
- };
286
+ const expression = parseFirstExpression(node);
287
+ return expression ? { type: "random", expression, source: node.source } : undefined;
292
288
  }
293
289
 
294
290
  if (node.localName === "qti-multiple") {
@@ -473,15 +469,16 @@ function parseExpression(node: XmlNode): QtiProcessingExpression | undefined {
473
469
  const [left, right] = childElements(node)
474
470
  .map(parseExpression)
475
471
  .filter((expression): expression is QtiProcessingExpression => expression !== undefined);
476
- if (left && right) return { type: "equal", left, right, source: node.source };
472
+ if (left && right)
473
+ return { type: "equal", left, right, attributes: node.attributes, source: node.source };
477
474
  }
478
475
 
479
476
  if (node.localName === "qti-equal-rounded") {
480
477
  const [left, right] = childElements(node)
481
478
  .map(parseExpression)
482
479
  .filter((expression): expression is QtiProcessingExpression => expression !== undefined);
483
- const roundingMode = node.attributes["rounding-mode"] ?? "";
484
- const figures = Number(node.attributes.figures ?? 0);
480
+ const roundingMode = node.attributes["rounding-mode"] ?? "significantFigures";
481
+ const figures = parseInteger(node.attributes.figures) ?? node.attributes.figures ?? "";
485
482
  if (left && right) {
486
483
  return { type: "equalRounded", left, right, roundingMode, figures, source: node.source };
487
484
  }
@@ -1,11 +1,11 @@
1
1
  import type {
2
2
  QtiBaseType,
3
3
  QtiCardinality,
4
- QtiRecordValue,
5
4
  QtiResponseDeclaration,
6
5
  QtiScalarValue,
7
6
  QtiValue,
8
7
  } from "./types.js";
8
+ import { isRecordValue } from "./value-guards.js";
9
9
 
10
10
  const QTI_BASE_TYPES = new Set<string>([
11
11
  "identifier",
@@ -127,7 +127,3 @@ export function parseCoords(value: string | undefined): number[] {
127
127
  .map((part) => Number(part.trim()))
128
128
  .filter((part) => Number.isFinite(part));
129
129
  }
130
-
131
- function isRecordValue(value: QtiValue): value is QtiRecordValue {
132
- return typeof value === "object" && value !== null && !Array.isArray(value);
133
- }
package/src/parser.ts CHANGED
@@ -178,6 +178,10 @@ function parseAssessmentItem(node: XmlNode, diagnostics: QtiDiagnostic[]): QtiAs
178
178
 
179
179
  return {
180
180
  identifier,
181
+ sourceChildren: node.children.map((child) => ({
182
+ qtiName: child.localName,
183
+ source: child.source,
184
+ })),
181
185
  title: node.attributes.title,
182
186
  language: node.attributes["xml:lang"] ?? node.attributes.lang,
183
187
  adaptive: parseXmlBoolean(node.attributes.adaptive) === true,
@@ -213,6 +217,36 @@ function diagnoseProcessingElements(
213
217
  if (!processingNode) return;
214
218
  for (const node of [processingNode, ...descendants(processingNode, () => true)]) {
215
219
  if (!node.localName.startsWith("qti-")) continue;
220
+ if (
221
+ node.localName === "qti-pattern-match" &&
222
+ (node.attributes.pattern === undefined || childElements(node).length !== 1)
223
+ ) {
224
+ diagnostics.push({
225
+ code: "processing.pattern.required",
226
+ severity: "error",
227
+ message: "qti-pattern-match requires a pattern attribute and exactly one expression.",
228
+ path: node.source.path,
229
+ source: node.source,
230
+ });
231
+ }
232
+ if (node.localName === "qti-is-null" && childElements(node).length !== 1) {
233
+ diagnostics.push({
234
+ code: "processing.isNull.arity",
235
+ severity: "error",
236
+ message: "qti-is-null requires exactly one expression.",
237
+ path: node.source.path,
238
+ source: node.source,
239
+ });
240
+ }
241
+ if (node.localName === "qti-random" && childElements(node).length !== 1) {
242
+ diagnostics.push({
243
+ code: "processing.random.arity",
244
+ severity: "error",
245
+ message: "qti-random requires exactly one container expression.",
246
+ path: node.source.path,
247
+ source: node.source,
248
+ });
249
+ }
216
250
  if (
217
251
  processingNode.localName === "qti-response-processing" &&
218
252
  responseProcessingForbiddenNames.has(node.localName)
@@ -0,0 +1,104 @@
1
+ import { parseFiniteNumber, parseXmlBoolean } from "./parser-values.js";
2
+ import type { QtiDiagnostic, QtiProcessingExpression, QtiValue } from "./types.js";
3
+
4
+ type EqualExpression = Extract<QtiProcessingExpression, { type: "equal" }>;
5
+ type EqualityTolerance =
6
+ | { mode: "exact" }
7
+ | {
8
+ mode: "absolute" | "relative";
9
+ lower: string;
10
+ upper: string;
11
+ includeLower: boolean;
12
+ includeUpper: boolean;
13
+ };
14
+
15
+ function parseTolerance(
16
+ attributes: Record<string, string> = {},
17
+ ): { ok: true; value: EqualityTolerance } | { ok: false; message: string } {
18
+ const mode = attributes["tolerance-mode"] ?? "exact";
19
+ if (mode !== "exact" && mode !== "absolute" && mode !== "relative") {
20
+ return {
21
+ ok: false,
22
+ message: "qti-equal requires exact, absolute, or relative tolerance-mode.",
23
+ };
24
+ }
25
+ const lowerAttribute = attributes["include-lower-bound"];
26
+ const upperAttribute = attributes["include-upper-bound"];
27
+ const includeLower = lowerAttribute === undefined ? true : parseXmlBoolean(lowerAttribute);
28
+ const includeUpper = upperAttribute === undefined ? true : parseXmlBoolean(upperAttribute);
29
+ if (includeLower === undefined || includeUpper === undefined) {
30
+ return { ok: false, message: "qti-equal boundary flags must be boolean." };
31
+ }
32
+ if (mode === "exact") return { ok: true, value: { mode } };
33
+ const tolerances = attributes.tolerance?.trim().split(/\s+/) ?? [];
34
+ const [lower, upper = lower] = tolerances;
35
+ if (!lower || !upper || tolerances.length > 2) {
36
+ return {
37
+ ok: false,
38
+ message: "qti-equal requires one or two tolerance values in absolute or relative mode.",
39
+ };
40
+ }
41
+ return { ok: true, value: { mode, lower, upper, includeLower, includeUpper } };
42
+ }
43
+
44
+ /** Validate tolerance attributes and numeric literal or declared-variable operands. */
45
+ export function validateEqualTolerance(
46
+ expression: EqualExpression,
47
+ variables: ReadonlySet<string>,
48
+ diagnostics: QtiDiagnostic[],
49
+ ): void {
50
+ const parsed = parseTolerance(expression.attributes);
51
+ const messages: string[] = [];
52
+ if (!parsed.ok) messages.push(parsed.message);
53
+ else if (parsed.value.mode !== "exact") {
54
+ for (const token of [parsed.value.lower, parsed.value.upper]) {
55
+ const number = parseFiniteNumber(token);
56
+ if (number === undefined ? !variables.has(token) : number < 0) {
57
+ messages.push(
58
+ "qti-equal tolerance must be nonnegative numeric values or declared variable references.",
59
+ );
60
+ break;
61
+ }
62
+ }
63
+ }
64
+ for (const message of messages) {
65
+ diagnostics.push({
66
+ code: "processing.equal.tolerance",
67
+ severity: "error",
68
+ message,
69
+ source: expression.source,
70
+ path: expression.source?.path,
71
+ });
72
+ }
73
+ }
74
+
75
+ /** Compare single numeric operands using QTI absolute/relative tolerance and boundary rules. */
76
+ export function equalWithTolerance(
77
+ expression: EqualExpression,
78
+ left: QtiValue,
79
+ right: QtiValue,
80
+ resolve: (identifier: string) => QtiValue,
81
+ ): boolean | null {
82
+ if (typeof left !== "number" || typeof right !== "number") return null;
83
+ const parsed = parseTolerance(expression.attributes);
84
+ if (!parsed.ok) return null;
85
+ const tolerance = parsed.value;
86
+ if (tolerance.mode === "exact") return left === right;
87
+ const lower = parseFiniteNumber(tolerance.lower) ?? resolve(tolerance.lower);
88
+ const upper = parseFiniteNumber(tolerance.upper) ?? resolve(tolerance.upper);
89
+ if (
90
+ typeof lower !== "number" ||
91
+ typeof upper !== "number" ||
92
+ !Number.isFinite(lower) ||
93
+ !Number.isFinite(upper) ||
94
+ lower < 0 ||
95
+ upper < 0
96
+ )
97
+ return null;
98
+ const minimum = tolerance.mode === "absolute" ? left - lower : left * (1 - lower / 100);
99
+ const maximum = tolerance.mode === "absolute" ? left + upper : left * (1 + upper / 100);
100
+ return (
101
+ (tolerance.includeLower ? right >= minimum : right > minimum) &&
102
+ (tolerance.includeUpper ? right <= maximum : right < maximum)
103
+ );
104
+ }
@@ -1,4 +1,5 @@
1
1
  import type { QtiProcessingExpression, QtiValue } from "./types.js";
2
+ import { equalWithTolerance } from "./processing-equality.js";
2
3
  import { assertNever } from "./assert-never.js";
3
4
  import type { EvaluationContext } from "./processing-evaluator.js";
4
5
  import {
@@ -47,22 +48,21 @@ export function evaluateComparisonExpression(
47
48
  case "equal": {
48
49
  const left = context.evaluate(expression.left);
49
50
  const right = context.evaluate(expression.right);
50
- return left === null || right === null
51
- ? null
52
- : valuesEqual(
53
- left,
54
- right,
55
- false,
56
- expressionBaseType(expression.left, context.document) ??
57
- expressionBaseType(expression.right, context.document),
58
- );
51
+ return equalWithTolerance(expression, left, right, (identifier) =>
52
+ context.evaluate({ type: "variable", identifier }),
53
+ );
59
54
  }
60
55
  case "equalRounded": {
56
+ const figures =
57
+ typeof expression.figures === "number"
58
+ ? expression.figures
59
+ : context.evaluate({ type: "variable", identifier: expression.figures });
61
60
  const validRounding =
62
- Number.isInteger(expression.figures) &&
61
+ typeof figures === "number" &&
62
+ Number.isInteger(figures) &&
63
63
  (expression.roundingMode === "decimalPlaces"
64
- ? expression.figures >= 0
65
- : expression.roundingMode === "significantFigures" && expression.figures > 0);
64
+ ? figures >= 0
65
+ : expression.roundingMode === "significantFigures" && figures > 0);
66
66
  if (!validRounding) return null;
67
67
  const left = context.evaluate(expression.left);
68
68
  const right = context.evaluate(expression.right);
@@ -70,8 +70,8 @@ export function evaluateComparisonExpression(
70
70
  const leftNumber = numericValueOrNull(left);
71
71
  const rightNumber = numericValueOrNull(right);
72
72
  if (leftNumber === null || rightNumber === null) return null;
73
- const roundedLeft = roundWithMode(leftNumber, expression.roundingMode, expression.figures);
74
- const roundedRight = roundWithMode(rightNumber, expression.roundingMode, expression.figures);
73
+ const roundedLeft = roundWithMode(leftNumber, expression.roundingMode, figures);
74
+ const roundedRight = roundWithMode(rightNumber, expression.roundingMode, figures);
75
75
  return roundedLeft === null || roundedRight === null ? null : roundedLeft === roundedRight;
76
76
  }
77
77
  case "numericCompare": {
@@ -123,6 +123,11 @@ export function evaluateComparisonExpression(
123
123
  const baseType =
124
124
  expressionBaseType(expression.collection, context.document) ??
125
125
  expressionBaseType(expression.values, context.document);
126
+ if (expressionIsOrdered(expression.collection, context.document)) {
127
+ return collection.some((_, offset) =>
128
+ valuesEqual(collection.slice(offset, offset + values.length), values, true, baseType),
129
+ );
130
+ }
126
131
  return containsValues(collection, values, baseType);
127
132
  }
128
133
  default:
@@ -9,12 +9,8 @@ import {
9
9
  roundToSignificantFigures,
10
10
  statsOperatorValue,
11
11
  } from "./processing-operators.js";
12
- import {
13
- isRecordValue,
14
- numericValue,
15
- numericValueOrNull,
16
- valueContainer,
17
- } from "./processing-values.js";
12
+ import { numericValue, numericValueOrNull, valueContainer } from "./processing-values.js";
13
+ import { isRecordValue } from "./value-guards.js";
18
14
 
19
15
  type NumericExpression = Extract<
20
16
  QtiProcessingExpression,
@@ -1,9 +1,9 @@
1
1
  import type { QtiProcessingExpression, QtiValue } from "./types.js";
2
2
  import { assertNever } from "./assert-never.js";
3
3
  import type { EvaluationContext } from "./processing-evaluator.js";
4
- import { qtiValueToString } from "./value-format.js";
5
- import { isRecordValue, stringMatch } from "./processing-values.js";
6
- import { resolveOptionalVariableValue } from "./processing-variables.js";
4
+ import { stringMatch } from "./processing-values.js";
5
+ import { isRecordValue } from "./value-guards.js";
6
+ import { matchXmlSchemaRegex, patternVariableIdentifier } from "./xml-schema-regex.js";
7
7
 
8
8
  type StringExpression = Extract<
9
9
  QtiProcessingExpression,
@@ -32,23 +32,30 @@ export function evaluateStringExpression(
32
32
  case "patternMatch": {
33
33
  const value = context.evaluate(expression.expression);
34
34
  if (value === null) return null;
35
- const patternValue =
36
- resolveOptionalVariableValue(
37
- context.document,
38
- expression.pattern,
39
- context.responses,
40
- context.outcomes,
41
- context.templateValues,
42
- ) ??
43
- context.undeclaredResponseValue(expression.pattern) ??
44
- expression.pattern;
45
- try {
46
- return new RegExp(
47
- typeof patternValue === "string" ? patternValue : qtiValueToString(patternValue),
48
- ).test(qtiValueToString(value));
49
- } catch {
35
+ const reference = patternVariableIdentifier(expression.pattern);
36
+ const pattern =
37
+ reference === undefined
38
+ ? expression.pattern
39
+ : context.evaluate({ type: "variable", identifier: reference });
40
+ if (pattern === null) return null;
41
+ if (typeof value !== "string" || typeof pattern !== "string") {
42
+ context.diagnostics.push({
43
+ code: "processing.pattern.type",
44
+ severity: "error",
45
+ message: "Pattern matching requires a single string pattern and operand.",
46
+ source: expression.source,
47
+ });
50
48
  return null;
51
49
  }
50
+ const result = matchXmlSchemaRegex(pattern, value);
51
+ if (result.ok) return result.matches;
52
+ context.diagnostics.push({
53
+ code: `processing.pattern.${result.problem.code}`,
54
+ severity: "error",
55
+ message: result.problem.message,
56
+ source: expression.source,
57
+ });
58
+ return null;
52
59
  }
53
60
  case "fieldValue": {
54
61
  const value = context.evaluate(expression.expression);
@@ -29,7 +29,7 @@ export function evaluateVariableExpression(
29
29
  ): QtiValue {
30
30
  switch (expression.type) {
31
31
  case "isNull":
32
- return isNullResponse(context.responses[expression.identifier] ?? null);
32
+ return isNullResponse(context.evaluate(expression.expression));
33
33
  case "matchCorrect": {
34
34
  const declaration = getResponseDeclaration(context.document, expression.correctIdentifier);
35
35
  return declaration
@@ -63,8 +63,12 @@ export function evaluateVariableExpression(
63
63
  case "correct":
64
64
  return context.correctResponses[expression.identifier] ?? null;
65
65
  case "default":
66
- return defaultValueForIdentifier(context.document, expression.identifier);
67
- case "variable":
66
+ return Object.hasOwn(context.defaultValues, expression.identifier)
67
+ ? (context.defaultValues[expression.identifier] ?? null)
68
+ : defaultValueForIdentifier(context.document, expression.identifier);
69
+ case "variable": {
70
+ const builtIn = context.builtInVariable(expression.identifier);
71
+ if (builtIn !== undefined) return builtIn;
68
72
  return (
69
73
  resolveVariableValue(
70
74
  context.document,
@@ -76,6 +80,7 @@ export function evaluateVariableExpression(
76
80
  context.undeclaredResponseValue(expression.identifier) ??
77
81
  null
78
82
  );
83
+ }
79
84
  default:
80
85
  return assertNever(expression);
81
86
  }
@@ -1,4 +1,4 @@
1
- import type { QtiDocument, QtiProcessingExpression, QtiValue } from "./types.js";
1
+ import type { QtiDiagnostic, QtiDocument, QtiProcessingExpression, QtiValue } from "./types.js";
2
2
  import { assertNever } from "./assert-never.js";
3
3
  import type { QtiCustomOperatorRegistry } from "./custom-operators.js";
4
4
  import { evaluateBooleanExpression } from "./processing-evaluator-boolean.js";
@@ -12,7 +12,8 @@ import { evaluateGeometryExpression } from "./processing-evaluator-geometry.js";
12
12
  import { evaluateNumericExpression } from "./processing-evaluator-numeric.js";
13
13
  import { evaluateStringExpression } from "./processing-evaluator-string.js";
14
14
  import { evaluateVariableExpression } from "./processing-evaluator-variable.js";
15
- import { isRecordValue, numericValueOrNull } from "./processing-values.js";
15
+ import { numericValueOrNull } from "./processing-values.js";
16
+ import { isRecordValue } from "./value-guards.js";
16
17
 
17
18
  export interface EvaluationContext {
18
19
  document: QtiDocument;
@@ -20,6 +21,9 @@ export interface EvaluationContext {
20
21
  outcomes: Record<string, QtiValue>;
21
22
  templateValues: Record<string, QtiValue>;
22
23
  correctResponses: Record<string, QtiValue>;
24
+ defaultValues: Record<string, QtiValue>;
25
+ diagnostics: QtiDiagnostic[];
26
+ builtInVariable(identifier: string): QtiValue | undefined;
23
27
  allowedUndeclaredResponseIdentifiers: ReadonlySet<string>;
24
28
  random: () => number;
25
29
  customOperators: QtiCustomOperatorRegistry;
@@ -30,6 +34,7 @@ export interface EvaluationContext {
30
34
  }
31
35
 
32
36
  export interface EvaluationOptions {
37
+ builtInVariable?: ((identifier: string) => QtiValue | undefined) | undefined;
33
38
  allowedUndeclaredResponseIdentifiers?: ReadonlySet<string> | readonly string[] | undefined;
34
39
  }
35
40
 
@@ -53,6 +58,14 @@ export function createEvaluationContext(
53
58
  outcomes,
54
59
  templateValues,
55
60
  correctResponses,
61
+ defaultValues: {},
62
+ diagnostics: [],
63
+ builtInVariable:
64
+ options.builtInVariable ??
65
+ ((identifier) =>
66
+ identifier === "completionStatus"
67
+ ? (outcomes.completionStatus ?? "not_attempted")
68
+ : undefined),
56
69
  allowedUndeclaredResponseIdentifiers,
57
70
  random,
58
71
  customOperators,
@@ -105,11 +118,11 @@ export function evaluateProcessingExpression(
105
118
  }
106
119
  case "randomFloat":
107
120
  return expression.min + context.random() * (expression.max - expression.min);
108
- case "random":
109
- if (expression.values.length === 0) return null;
110
- return context.evaluate(
111
- expression.values[Math.floor(context.random() * expression.values.length)]!,
112
- );
121
+ case "random": {
122
+ const values = context.evaluate(expression.expression);
123
+ if (!Array.isArray(values) || values.length === 0) return null;
124
+ return values[Math.floor(context.random() * values.length)] ?? null;
125
+ }
113
126
  case "isNull":
114
127
  case "matchCorrect":
115
128
  case "mapResponse":
@@ -3,8 +3,6 @@ import { assertNever } from "./assert-never.js";
3
3
 
4
4
  export function expressionChildren(expression: QtiProcessingExpression): QtiProcessingExpression[] {
5
5
  switch (expression.type) {
6
- case "random":
7
- return expression.values;
8
6
  case "multiple":
9
7
  case "ordered":
10
8
  case "sum":
@@ -33,6 +31,8 @@ export function expressionChildren(expression: QtiProcessingExpression): QtiProc
33
31
  case "stringMatch":
34
32
  case "substring":
35
33
  return [expression.left, expression.right];
34
+ case "isNull":
35
+ case "random":
36
36
  case "not":
37
37
  case "round":
38
38
  case "roundTo":
@@ -54,7 +54,6 @@ export function expressionChildren(expression: QtiProcessingExpression): QtiProc
54
54
  case "baseValue":
55
55
  case "correct":
56
56
  case "default":
57
- case "isNull":
58
57
  case "mapResponse":
59
58
  case "mapResponsePoint":
60
59
  case "matchCorrect":
@@ -17,6 +17,8 @@ export const identifierExpressionTags = {
17
17
  export type IdentifierExpressionType = keyof typeof identifierExpressionTags;
18
18
 
19
19
  export const unaryExpressionTags = {
20
+ isNull: "qti-is-null",
21
+ random: "qti-random",
20
22
  containerSize: "qti-container-size",
21
23
  round: "qti-round",
22
24
  truncate: "qti-truncate",
@@ -47,7 +49,6 @@ export const leftRightExpressionTags = {
47
49
  power: "qti-power",
48
50
  integerDivide: "qti-integer-divide",
49
51
  integerModulus: "qti-integer-modulus",
50
- equal: "qti-equal",
51
52
  } as const;
52
53
 
53
54
  export type LeftRightExpressionType = keyof typeof leftRightExpressionTags;