@longsightgroup/qti3-core 0.10.6 → 0.11.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.
- package/README.md +126 -0
- package/dist/adaptive-turn-materializer.js +4 -5
- package/dist/adaptive-turn-materializer.js.map +1 -1
- package/dist/adaptive-turn.d.ts.map +1 -1
- package/dist/adaptive-turn.js +1 -0
- package/dist/adaptive-turn.js.map +1 -1
- package/dist/attempt-state.d.ts.map +1 -1
- package/dist/attempt-state.js +41 -1
- package/dist/attempt-state.js.map +1 -1
- package/dist/diagnostics.d.ts +13 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +28 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/item-submission-materialization.d.ts.map +1 -1
- package/dist/item-submission-materialization.js +2 -0
- package/dist/item-submission-materialization.js.map +1 -1
- package/dist/parser-processing.d.ts.map +1 -1
- package/dist/parser-processing.js +8 -18
- package/dist/parser-processing.js.map +1 -1
- package/dist/parser-values.d.ts.map +1 -1
- package/dist/parser-values.js +1 -3
- package/dist/parser-values.js.map +1 -1
- package/dist/parser.js +32 -0
- package/dist/parser.js.map +1 -1
- package/dist/processing-equality.d.ts +10 -0
- package/dist/processing-equality.d.ts.map +1 -0
- package/dist/processing-equality.js +78 -0
- package/dist/processing-equality.js.map +1 -0
- package/dist/processing-evaluator-comparison.d.ts.map +1 -1
- package/dist/processing-evaluator-comparison.js +14 -9
- package/dist/processing-evaluator-comparison.js.map +1 -1
- package/dist/processing-evaluator-numeric.d.ts.map +1 -1
- package/dist/processing-evaluator-numeric.js +2 -1
- package/dist/processing-evaluator-numeric.js.map +1 -1
- package/dist/processing-evaluator-string.d.ts.map +1 -1
- package/dist/processing-evaluator-string.js +26 -10
- package/dist/processing-evaluator-string.js.map +1 -1
- package/dist/processing-evaluator-variable.d.ts.map +1 -1
- package/dist/processing-evaluator-variable.js +9 -3
- package/dist/processing-evaluator-variable.js.map +1 -1
- package/dist/processing-evaluator.d.ts +5 -1
- package/dist/processing-evaluator.d.ts.map +1 -1
- package/dist/processing-evaluator.js +13 -4
- package/dist/processing-evaluator.js.map +1 -1
- package/dist/processing-expression-children.d.ts.map +1 -1
- package/dist/processing-expression-children.js +2 -3
- package/dist/processing-expression-children.js.map +1 -1
- package/dist/processing-expression-tags.d.ts +2 -1
- package/dist/processing-expression-tags.d.ts.map +1 -1
- package/dist/processing-expression-tags.js +2 -1
- package/dist/processing-expression-tags.js.map +1 -1
- package/dist/processing-mapping.d.ts.map +1 -1
- package/dist/processing-mapping.js +26 -16
- package/dist/processing-mapping.js.map +1 -1
- package/dist/processing-state.d.ts +2 -2
- package/dist/processing-state.d.ts.map +1 -1
- package/dist/processing-state.js +5 -2
- package/dist/processing-state.js.map +1 -1
- package/dist/processing-values.d.ts +1 -2
- package/dist/processing-values.d.ts.map +1 -1
- package/dist/processing-values.js +1 -3
- package/dist/processing-values.js.map +1 -1
- package/dist/processing-variables.d.ts.map +1 -1
- package/dist/processing-variables.js +10 -5
- package/dist/processing-variables.js.map +1 -1
- package/dist/qti-package-assets.d.ts.map +1 -1
- package/dist/qti-package-assets.js +16 -10
- package/dist/qti-package-assets.js.map +1 -1
- package/dist/qti-package-manifest.d.ts +1 -0
- package/dist/qti-package-manifest.d.ts.map +1 -1
- package/dist/qti-package-manifest.js +1 -0
- package/dist/qti-package-manifest.js.map +1 -1
- package/dist/qti-package-metadata.d.ts.map +1 -1
- package/dist/qti-package-metadata.js +11 -0
- package/dist/qti-package-metadata.js.map +1 -1
- package/dist/qti-package-plan.d.ts +1 -1
- package/dist/qti-package-plan.d.ts.map +1 -1
- package/dist/qti-package-types.d.ts +9 -0
- package/dist/qti-package-types.d.ts.map +1 -1
- package/dist/qti-package-xml.d.ts +6 -1
- package/dist/qti-package-xml.d.ts.map +1 -1
- package/dist/qti-package-xml.js +11 -6
- package/dist/qti-package-xml.js.map +1 -1
- package/dist/qti-package-zip.d.ts +13 -1
- package/dist/qti-package-zip.d.ts.map +1 -1
- package/dist/qti-package-zip.js +185 -54
- package/dist/qti-package-zip.js.map +1 -1
- package/dist/qti-package.d.ts +12 -5
- package/dist/qti-package.d.ts.map +1 -1
- package/dist/qti-package.js +63 -8
- package/dist/qti-package.js.map +1 -1
- package/dist/response-validation-policy.d.ts.map +1 -1
- package/dist/response-validation-policy.js +3 -1
- package/dist/response-validation-policy.js.map +1 -1
- package/dist/response-validation.d.ts.map +1 -1
- package/dist/response-validation.js +2 -1
- package/dist/response-validation.js.map +1 -1
- package/dist/serializer-processing-expressions.d.ts.map +1 -1
- package/dist/serializer-processing-expressions.js +2 -10
- package/dist/serializer-processing-expressions.js.map +1 -1
- package/dist/server-scoring.d.ts.map +1 -1
- package/dist/server-scoring.js +1 -0
- package/dist/server-scoring.js.map +1 -1
- package/dist/session-builtins.d.ts +16 -0
- package/dist/session-builtins.d.ts.map +1 -0
- package/dist/session-builtins.js +58 -0
- package/dist/session-builtins.js.map +1 -0
- package/dist/session.d.ts +5 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +63 -11
- package/dist/session.js.map +1 -1
- package/dist/support-evidence.d.ts.map +1 -1
- package/dist/support-evidence.js +2 -0
- package/dist/support-evidence.js.map +1 -1
- package/dist/support.d.ts.map +1 -1
- package/dist/support.js +64 -23
- package/dist/support.js.map +1 -1
- package/dist/trusted-item-session.d.ts +4 -1
- package/dist/trusted-item-session.d.ts.map +1 -1
- package/dist/trusted-item-session.js +8 -3
- package/dist/trusted-item-session.js.map +1 -1
- package/dist/types.d.ts +21 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/validation-declaration-entries.d.ts.map +1 -1
- package/dist/validation-declaration-entries.js +11 -1
- package/dist/validation-declaration-entries.js.map +1 -1
- package/dist/validation-item-children.d.ts +4 -0
- package/dist/validation-item-children.d.ts.map +1 -0
- package/dist/validation-item-children.js +45 -0
- package/dist/validation-item-children.js.map +1 -0
- package/dist/validation-processing.d.ts.map +1 -1
- package/dist/validation-processing.js +45 -12
- package/dist/validation-processing.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +12 -0
- package/dist/validation.js.map +1 -1
- package/dist/value-guards.d.ts +4 -0
- package/dist/value-guards.d.ts.map +1 -0
- package/dist/value-guards.js +5 -0
- package/dist/value-guards.js.map +1 -0
- package/dist/xml-schema-regex-data.d.ts +48 -0
- package/dist/xml-schema-regex-data.d.ts.map +1 -0
- package/dist/xml-schema-regex-data.js +403 -0
- package/dist/xml-schema-regex-data.js.map +1 -0
- package/dist/xml-schema-regex-parser.d.ts +31 -0
- package/dist/xml-schema-regex-parser.d.ts.map +1 -0
- package/dist/xml-schema-regex-parser.js +245 -0
- package/dist/xml-schema-regex-parser.js.map +1 -0
- package/dist/xml-schema-regex.d.ts +14 -0
- package/dist/xml-schema-regex.d.ts.map +1 -0
- package/dist/xml-schema-regex.js +114 -0
- package/dist/xml-schema-regex.js.map +1 -0
- package/package.json +1 -1
- package/src/adaptive-turn-materializer.ts +4 -3
- package/src/adaptive-turn.ts +1 -0
- package/src/attempt-state.ts +54 -1
- package/src/diagnostics.ts +29 -0
- package/src/index.ts +9 -0
- package/src/item-submission-materialization.ts +2 -0
- package/src/parser-processing.ts +15 -18
- package/src/parser-values.ts +1 -5
- package/src/parser.ts +34 -0
- package/src/processing-equality.ts +104 -0
- package/src/processing-evaluator-comparison.ts +19 -14
- package/src/processing-evaluator-numeric.ts +2 -6
- package/src/processing-evaluator-string.ts +25 -18
- package/src/processing-evaluator-variable.ts +8 -3
- package/src/processing-evaluator.ts +20 -7
- package/src/processing-expression-children.ts +2 -3
- package/src/processing-expression-tags.ts +2 -1
- package/src/processing-mapping.ts +31 -25
- package/src/processing-state.ts +6 -1
- package/src/processing-values.ts +2 -11
- package/src/processing-variables.ts +9 -7
- package/src/qti-package-assets.ts +20 -13
- package/src/qti-package-manifest.ts +1 -0
- package/src/qti-package-metadata.ts +11 -0
- package/src/qti-package-plan.ts +1 -1
- package/src/qti-package-types.ts +10 -0
- package/src/qti-package-xml.ts +11 -6
- package/src/qti-package-zip.ts +330 -97
- package/src/qti-package.ts +106 -7
- package/src/response-validation-policy.ts +5 -1
- package/src/response-validation.ts +2 -1
- package/src/serializer-processing-expressions.ts +8 -25
- package/src/server-scoring.ts +1 -0
- package/src/session-builtins.ts +75 -0
- package/src/session.ts +64 -8
- package/src/support-evidence.ts +2 -0
- package/src/support.ts +84 -23
- package/src/trusted-item-session.ts +13 -2
- package/src/types.ts +30 -4
- package/src/validation-declaration-entries.ts +11 -1
- package/src/validation-item-children.ts +48 -0
- package/src/validation-processing.ts +47 -10
- package/src/validation.ts +14 -0
- package/src/value-guards.ts +6 -0
- package/src/xml-schema-regex-data.ts +404 -0
- package/src/xml-schema-regex-parser.ts +234 -0
- package/src/xml-schema-regex.ts +115 -0
package/src/support.ts
CHANGED
|
@@ -92,7 +92,11 @@ export const processingSupport: QtiProcessingElementSupport[] = [
|
|
|
92
92
|
"packages/fixtures/xml/random-integer-template-reference.xml",
|
|
93
93
|
"packages/fixtures/xml/template-content-reference.xml",
|
|
94
94
|
]),
|
|
95
|
-
|
|
95
|
+
{
|
|
96
|
+
...templateProcessingEntry("qti-set-default-value"),
|
|
97
|
+
notes:
|
|
98
|
+
"Updates effective response/outcome defaults for the clone, including explicit NULL overrides; rejected template passes discard their overrides.",
|
|
99
|
+
},
|
|
96
100
|
templateProcessingEntry("qti-set-correct-response", [
|
|
97
101
|
"packages/fixtures/xml/template-processing-reference.xml",
|
|
98
102
|
"packages/fixtures/xml/random-integer-template-reference.xml",
|
|
@@ -133,32 +137,65 @@ export const processingSupport: QtiProcessingElementSupport[] = [
|
|
|
133
137
|
operatorProcessingEntry("qti-null"),
|
|
134
138
|
mappingProcessingEntry("qti-match-table"),
|
|
135
139
|
mappingProcessingEntry("qti-match-table-entry"),
|
|
136
|
-
|
|
140
|
+
{
|
|
141
|
+
...mappingProcessingEntry("qti-interpolation-table"),
|
|
142
|
+
notes:
|
|
143
|
+
"Selects the first authored entry whose source-value is below the input, including equality only when include-boundary permits it.",
|
|
144
|
+
},
|
|
137
145
|
mappingProcessingEntry("qti-interpolation-table-entry"),
|
|
138
|
-
|
|
146
|
+
{
|
|
147
|
+
...operatorProcessingEntry("qti-is-null"),
|
|
148
|
+
notes:
|
|
149
|
+
"Accepts one arbitrary expression, evaluates it in the full variable context, and tests the result for NULL.",
|
|
150
|
+
},
|
|
139
151
|
mappingProcessingEntry("qti-match", [
|
|
140
152
|
"packages/fixtures/xml/generic-match-processing-reference.xml",
|
|
141
153
|
"packages/fixtures/xml/advanced-processing-reference.xml",
|
|
142
154
|
]),
|
|
143
155
|
mappingProcessingEntry("qti-correct"),
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
"
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
156
|
+
{
|
|
157
|
+
...mappingProcessingEntry("qti-default", [], [processingTemplateTest]),
|
|
158
|
+
notes:
|
|
159
|
+
"Reads the effective authored or template-overridden default, independently of the variable's current value.",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
...mappingProcessingEntry("qti-map-response"),
|
|
163
|
+
notes:
|
|
164
|
+
"String map entries honor case-sensitive (default false); mapping counts each distinct response value once using QTI base-type equality, then applies bounds.",
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
...mappingProcessingEntry("qti-map-response-point"),
|
|
168
|
+
notes:
|
|
169
|
+
"Each point selects the first matching authored area; each selected area contributes at most once.",
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
...responseProcessingEntry("qti-variable", [
|
|
173
|
+
"packages/fixtures/xml/generic-match-processing-reference.xml",
|
|
174
|
+
"packages/fixtures/xml/template-processing-reference.xml",
|
|
175
|
+
"packages/fixtures/xml/random-integer-template-reference.xml",
|
|
176
|
+
"packages/fixtures/xml/template-content-reference.xml",
|
|
177
|
+
"packages/fixtures/xml/advanced-processing-reference.xml",
|
|
178
|
+
"packages/fixtures/xml/adaptive-feedback-reference.xml",
|
|
179
|
+
]),
|
|
180
|
+
notes:
|
|
181
|
+
"Resolves declared variables and implicit completionStatus, numAttempts, timed-item duration (float seconds), and QTI_CONTEXT; session environment is resolved before template processing.",
|
|
182
|
+
tests: [
|
|
183
|
+
processingResponseTest,
|
|
184
|
+
"packages/core/src/session-builtins.test.ts",
|
|
185
|
+
"tests/browser/player-lifecycle.spec.ts",
|
|
186
|
+
],
|
|
187
|
+
},
|
|
155
188
|
templateProcessingEntry(
|
|
156
189
|
"qti-random-integer",
|
|
157
190
|
["packages/fixtures/xml/random-integer-template-reference.xml"],
|
|
158
191
|
[...processingBrowserEvidence["qti-random-integer"]],
|
|
159
192
|
),
|
|
160
193
|
operatorProcessingEntry("qti-random-float"),
|
|
161
|
-
|
|
194
|
+
{
|
|
195
|
+
...operatorProcessingEntry("qti-random"),
|
|
196
|
+
notes:
|
|
197
|
+
"Evaluates one multiple/ordered container expression, including variables, then selects a scalar member; NULL/empty containers return NULL and serialization retains the operand.",
|
|
198
|
+
},
|
|
162
199
|
operatorProcessingEntry("qti-multiple", [
|
|
163
200
|
"packages/fixtures/xml/advanced-processing-reference.xml",
|
|
164
201
|
]),
|
|
@@ -166,7 +203,15 @@ export const processingSupport: QtiProcessingElementSupport[] = [
|
|
|
166
203
|
"packages/fixtures/xml/advanced-processing-reference.xml",
|
|
167
204
|
]),
|
|
168
205
|
operatorProcessingEntry("qti-index"),
|
|
169
|
-
|
|
206
|
+
{
|
|
207
|
+
...operatorProcessingEntry(
|
|
208
|
+
"qti-contains",
|
|
209
|
+
[],
|
|
210
|
+
["packages/core/src/processing-containment.test.ts"],
|
|
211
|
+
),
|
|
212
|
+
notes:
|
|
213
|
+
"Multiple containers use multiset containment; ordered containers require a contiguous sequence and preserve duplicate counts, including variable/delete/repeat operands.",
|
|
214
|
+
},
|
|
170
215
|
operatorProcessingEntry("qti-container-size", [
|
|
171
216
|
"packages/fixtures/xml/advanced-processing-reference.xml",
|
|
172
217
|
]),
|
|
@@ -192,10 +237,20 @@ export const processingSupport: QtiProcessingElementSupport[] = [
|
|
|
192
237
|
operatorProcessingEntry("qti-any-n"),
|
|
193
238
|
operatorProcessingEntry("qti-or"),
|
|
194
239
|
operatorProcessingEntry("qti-not"),
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
240
|
+
{
|
|
241
|
+
...operatorProcessingEntry("qti-equal", [], ["packages/core/src/processing-equality.test.ts"]),
|
|
242
|
+
notes:
|
|
243
|
+
"Preserves and evaluates exact, absolute, and relative tolerance with asymmetric bounds, variable tolerances, and inclusive/exclusive boundaries.",
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
...operatorProcessingEntry(
|
|
247
|
+
"qti-equal-rounded",
|
|
248
|
+
["packages/fixtures/xml/advanced-processing-reference.xml"],
|
|
249
|
+
["packages/core/src/processing-equality.test.ts"],
|
|
250
|
+
),
|
|
251
|
+
notes:
|
|
252
|
+
"Defaults rounding-mode to significantFigures and resolves integer-or-variable-reference figures at evaluation time, preserving both through serialization.",
|
|
253
|
+
},
|
|
199
254
|
mappingProcessingEntry("qti-field-value"),
|
|
200
255
|
operatorProcessingEntry("qti-lt"),
|
|
201
256
|
operatorProcessingEntry("qti-lte"),
|
|
@@ -203,7 +258,12 @@ export const processingSupport: QtiProcessingElementSupport[] = [
|
|
|
203
258
|
operatorProcessingEntry("qti-gte"),
|
|
204
259
|
operatorProcessingEntry("qti-string-match"),
|
|
205
260
|
operatorProcessingEntry("qti-substring"),
|
|
206
|
-
|
|
261
|
+
{
|
|
262
|
+
...operatorProcessingEntry("qti-pattern-match"),
|
|
263
|
+
tests: [processingOperatorsTest, "packages/core/src/processing-pattern.test.ts"],
|
|
264
|
+
notes:
|
|
265
|
+
"XML Schema Appendix F whole-string matching, XML name escapes, Unicode categories/blocks and class subtraction; brace-enclosed pattern variables, NULL propagation and explicit syntax/resource diagnostics. No JavaScript-pattern fallback.",
|
|
266
|
+
},
|
|
207
267
|
operatorProcessingEntry("qti-member"),
|
|
208
268
|
operatorProcessingEntry("qti-delete"),
|
|
209
269
|
operatorProcessingEntry("qti-duration-gte"),
|
|
@@ -272,10 +332,11 @@ export const itemMetadataSupport: QtiItemMetadataElementSupport[] = [
|
|
|
272
332
|
tests: [
|
|
273
333
|
processingResponseTest,
|
|
274
334
|
"packages/core/src/parser-item-metadata.test.ts",
|
|
335
|
+
"packages/core/src/adaptive-turn-materializer.test.ts",
|
|
275
336
|
...browserFeedbackTests,
|
|
276
337
|
],
|
|
277
338
|
notes:
|
|
278
|
-
"Parsed by parser-item-metadata.ts and validated by validateModalFeedback. Response processing supplies the outcome
|
|
339
|
+
"Parsed by parser-item-metadata.ts and validated by validateModalFeedback. Response processing supplies the scalar/container outcome used for show/hide visibility, including inverse hide conditions, in both player rendering and adaptive candidate XML materialization.",
|
|
279
340
|
},
|
|
280
341
|
{
|
|
281
342
|
qtiName: "qti-companion-materials-info",
|
|
@@ -406,7 +467,7 @@ function entry(qtiName: string, interactionType: QtiInteractionType): QtiInterac
|
|
|
406
467
|
? "Supports single and multiple unordered pair responses."
|
|
407
468
|
: "Supports single and multiple directedPair responses."
|
|
408
469
|
: interactionType === "graphicGapMatch"
|
|
409
|
-
? "Requires multiple directedPair responses."
|
|
470
|
+
? "Requires multiple directedPair responses; multiple and repeated placements in one hotspot preserve each association through rendering, removal, scoring, and restoration, subject to source/target match-max and max-associations."
|
|
410
471
|
: undefined,
|
|
411
472
|
};
|
|
412
473
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { QtiSessionEnvironment } from "./session-builtins.js";
|
|
1
2
|
import { parseQtiXml } from "./parser.js";
|
|
2
3
|
import { itemExpectsAutomatedScore, itemHasExternalScore } from "./item-scoring-expectation.js";
|
|
3
4
|
import { parseQtiResponseVariables } from "./response-validation.js";
|
|
@@ -31,6 +32,8 @@ export interface QtiTrustedResponseApplication {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
export interface QtiTrustedItemParseOptions {
|
|
35
|
+
/** Trusted host environment; never take duration/context from candidate response fields. */
|
|
36
|
+
sessionEnvironment?: QtiSessionEnvironment | undefined;
|
|
34
37
|
allowedUndeclaredResponseIdentifiers?: readonly string[] | undefined;
|
|
35
38
|
}
|
|
36
39
|
|
|
@@ -48,7 +51,7 @@ export interface RunTrustedItemSessionInput extends QtiTrustedItemParseOptions {
|
|
|
48
51
|
diagnosticPrefix: QtiTrustedInputDiagnosticPrefix;
|
|
49
52
|
submission: QtiTrustedResponseApplication;
|
|
50
53
|
priorState?: QtiAttemptStateV1 | null | undefined;
|
|
51
|
-
/** Applied before submission.
|
|
54
|
+
/** Applied before submission, except completed is applied after capture. For server scoring, not adaptive turns. */
|
|
52
55
|
attemptStatus?: QtiAttemptStatus | undefined;
|
|
53
56
|
scoring: QtiTrustedItemScoringPolicy;
|
|
54
57
|
requireNumericScore: boolean;
|
|
@@ -156,12 +159,15 @@ export function runTrustedItemSession(
|
|
|
156
159
|
input.diagnosticPrefix,
|
|
157
160
|
input.allowedUndeclaredResponseIdentifiers,
|
|
158
161
|
parsedResult.parsed.diagnostics,
|
|
162
|
+
input.sessionEnvironment,
|
|
159
163
|
);
|
|
160
164
|
if (!sessionResult.ok) {
|
|
161
165
|
return emptyTrustedItemSessionFailure(sessionResult.diagnostics);
|
|
162
166
|
}
|
|
163
167
|
|
|
164
|
-
if (input.attemptStatus
|
|
168
|
+
if (input.attemptStatus && input.attemptStatus !== "completed") {
|
|
169
|
+
sessionResult.session.setStatus(input.attemptStatus);
|
|
170
|
+
}
|
|
165
171
|
|
|
166
172
|
let submission = input.submission;
|
|
167
173
|
if (input.submissionValidation === "strict") {
|
|
@@ -189,6 +195,9 @@ export function runTrustedItemSession(
|
|
|
189
195
|
return emptyTrustedItemSessionFailure(diagnostics);
|
|
190
196
|
}
|
|
191
197
|
|
|
198
|
+
// Capture the submitted attempt before closing it, so built-in counters reflect participation.
|
|
199
|
+
if (input.attemptStatus === "completed") sessionResult.session.setStatus("completed");
|
|
200
|
+
|
|
192
201
|
const shouldScore = input.scoring === "always" || applicationResult.appliedSubmission;
|
|
193
202
|
const externalScore = itemHasExternalScore(parsedResult.parsed.document.item);
|
|
194
203
|
|
|
@@ -277,11 +286,13 @@ function createTrustedItemSession(
|
|
|
277
286
|
diagnosticPrefix: QtiTrustedInputDiagnosticPrefix,
|
|
278
287
|
allowedUndeclaredResponseIdentifiers: readonly string[] | undefined,
|
|
279
288
|
existingDiagnostics: QtiDiagnostic[] = [],
|
|
289
|
+
sessionEnvironment: QtiSessionEnvironment = {},
|
|
280
290
|
): { ok: true; session: QtiItemSession } | { ok: false; diagnostics: QtiDiagnostic[] } {
|
|
281
291
|
try {
|
|
282
292
|
return {
|
|
283
293
|
ok: true,
|
|
284
294
|
session: createItemSession(parsed.document, priorState ?? undefined, {
|
|
295
|
+
...sessionEnvironment,
|
|
285
296
|
allowedUndeclaredResponseIdentifiers,
|
|
286
297
|
}),
|
|
287
298
|
};
|
package/src/types.ts
CHANGED
|
@@ -459,6 +459,13 @@ export interface QtiAssessmentItem {
|
|
|
459
459
|
stylesheets: QtiStylesheet[];
|
|
460
460
|
body: QtiContentNode[];
|
|
461
461
|
bodyText: string;
|
|
462
|
+
/** Authored direct children in XML order; omitted for items created without source XML. */
|
|
463
|
+
sourceChildren?:
|
|
464
|
+
| readonly {
|
|
465
|
+
readonly qtiName: string;
|
|
466
|
+
readonly source?: QtiSourceLocation | undefined;
|
|
467
|
+
}[]
|
|
468
|
+
| undefined;
|
|
462
469
|
source?: QtiSourceLocation | undefined;
|
|
463
470
|
}
|
|
464
471
|
|
|
@@ -593,7 +600,7 @@ export type QtiProcessingExpression = (
|
|
|
593
600
|
baseType?: string | undefined;
|
|
594
601
|
}
|
|
595
602
|
| { type: "null" }
|
|
596
|
-
| { type: "isNull";
|
|
603
|
+
| { type: "isNull"; expression: QtiProcessingExpression }
|
|
597
604
|
| { type: "matchCorrect"; identifier: string; correctIdentifier: string }
|
|
598
605
|
| { type: "match"; left: QtiProcessingExpression; right: QtiProcessingExpression }
|
|
599
606
|
| { type: "correct"; identifier: string }
|
|
@@ -614,7 +621,7 @@ export type QtiProcessingExpression = (
|
|
|
614
621
|
max: number;
|
|
615
622
|
attributes: Record<string, string>;
|
|
616
623
|
}
|
|
617
|
-
| { type: "random";
|
|
624
|
+
| { type: "random"; expression: QtiProcessingExpression }
|
|
618
625
|
| { type: "multiple"; expressions: QtiProcessingExpression[] }
|
|
619
626
|
| { type: "ordered"; expressions: QtiProcessingExpression[] }
|
|
620
627
|
| { type: "index"; expression: QtiProcessingExpression; n: string }
|
|
@@ -641,13 +648,18 @@ export type QtiProcessingExpression = (
|
|
|
641
648
|
| { type: "anyN"; expressions: QtiProcessingExpression[]; min: string; max: string }
|
|
642
649
|
| { type: "or"; expressions: QtiProcessingExpression[] }
|
|
643
650
|
| { type: "not"; expression: QtiProcessingExpression }
|
|
644
|
-
| {
|
|
651
|
+
| {
|
|
652
|
+
type: "equal";
|
|
653
|
+
left: QtiProcessingExpression;
|
|
654
|
+
right: QtiProcessingExpression;
|
|
655
|
+
attributes?: Record<string, string> | undefined;
|
|
656
|
+
}
|
|
645
657
|
| {
|
|
646
658
|
type: "equalRounded";
|
|
647
659
|
left: QtiProcessingExpression;
|
|
648
660
|
right: QtiProcessingExpression;
|
|
649
661
|
roundingMode: string;
|
|
650
|
-
figures: number;
|
|
662
|
+
figures: number | string;
|
|
651
663
|
}
|
|
652
664
|
| {
|
|
653
665
|
type: "numericCompare";
|
|
@@ -724,8 +736,22 @@ export interface QtiValidationResult {
|
|
|
724
736
|
diagnostics: QtiDiagnostic[];
|
|
725
737
|
}
|
|
726
738
|
|
|
739
|
+
export interface QtiSessionContext {
|
|
740
|
+
candidateIdentifier: string;
|
|
741
|
+
testIdentifier: string;
|
|
742
|
+
environmentIdentifier: string;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
export interface QtiBuiltInVariables {
|
|
746
|
+
numAttempts: number;
|
|
747
|
+
duration: number | null;
|
|
748
|
+
attemptInProgress: boolean;
|
|
749
|
+
context: QtiSessionContext;
|
|
750
|
+
}
|
|
751
|
+
|
|
727
752
|
export interface QtiAttemptStateV1 {
|
|
728
753
|
schema: "qti3.attempt-state.v1";
|
|
754
|
+
builtInVariables?: QtiBuiltInVariables | undefined;
|
|
729
755
|
itemIdentifier: string;
|
|
730
756
|
status: QtiAttemptStatus;
|
|
731
757
|
responses: Record<string, QtiValue>;
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
isNumericCsv,
|
|
6
6
|
numericCsv,
|
|
7
7
|
} from "./validation-geometry.js";
|
|
8
|
-
import { isFiniteNumber, isInteger } from "./validation-primitives.js";
|
|
8
|
+
import { isBooleanAttribute, isFiniteNumber, isInteger } from "./validation-primitives.js";
|
|
9
9
|
|
|
10
10
|
interface DeclarationEntryAttributes {
|
|
11
11
|
attributes: Record<string, string>;
|
|
@@ -18,6 +18,16 @@ export function validateMapEntryAttributes(
|
|
|
18
18
|
entry: DeclarationEntryAttributes,
|
|
19
19
|
diagnostics: QtiDiagnostic[],
|
|
20
20
|
): void {
|
|
21
|
+
const caseSensitive = entry.attributes["case-sensitive"];
|
|
22
|
+
if (caseSensitive !== undefined && !isBooleanAttribute(caseSensitive)) {
|
|
23
|
+
diagnostics.push({
|
|
24
|
+
code: "mapEntry.caseSensitive",
|
|
25
|
+
severity: "error",
|
|
26
|
+
message: `Response declaration ${declarationIdentifier} map entry requires a boolean case-sensitive value.`,
|
|
27
|
+
path: entry.source?.path,
|
|
28
|
+
source: entry.source,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
21
31
|
if (!entry.attributes["map-key"]) {
|
|
22
32
|
diagnostics.push({
|
|
23
33
|
code: "mapEntry.mapKey.required",
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { QtiAssessmentItem, QtiDiagnostic } from "./types.js";
|
|
2
|
+
|
|
3
|
+
// QTI 3.0.1 AssessmentItemDType sequence:
|
|
4
|
+
// https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_itemv3p0p1_v1p0.xsd
|
|
5
|
+
const itemChildOrder: readonly string[] = [
|
|
6
|
+
"qti-context-declaration",
|
|
7
|
+
"qti-response-declaration",
|
|
8
|
+
"qti-outcome-declaration",
|
|
9
|
+
"qti-template-declaration",
|
|
10
|
+
"qti-template-processing",
|
|
11
|
+
"qti-assessment-stimulus-ref",
|
|
12
|
+
"qti-companion-materials-info",
|
|
13
|
+
"qti-stylesheet",
|
|
14
|
+
"qti-item-body",
|
|
15
|
+
"qti-catalog-info",
|
|
16
|
+
"qti-response-processing",
|
|
17
|
+
"qti-modal-feedback",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/** Validate the authored direct-child sequence retained by the XML parser. */
|
|
21
|
+
export function validateAssessmentItemChildren(
|
|
22
|
+
item: QtiAssessmentItem,
|
|
23
|
+
diagnostics: QtiDiagnostic[],
|
|
24
|
+
): void {
|
|
25
|
+
let lastOrder = -1;
|
|
26
|
+
for (const child of item.sourceChildren ?? []) {
|
|
27
|
+
const order = itemChildOrder.indexOf(child.qtiName);
|
|
28
|
+
if (order < 0) {
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
code: "assessmentItem.child.unsupported",
|
|
31
|
+
severity: "error",
|
|
32
|
+
message: `qti-assessment-item does not allow ${child.qtiName} as a direct child.`,
|
|
33
|
+
path: child.source?.path,
|
|
34
|
+
source: child.source,
|
|
35
|
+
});
|
|
36
|
+
} else if (order < lastOrder) {
|
|
37
|
+
diagnostics.push({
|
|
38
|
+
code: "assessmentItem.child.order",
|
|
39
|
+
severity: "error",
|
|
40
|
+
message: `${child.qtiName} appears out of QTI 3 qti-assessment-item child order.`,
|
|
41
|
+
path: child.source?.path,
|
|
42
|
+
source: child.source,
|
|
43
|
+
});
|
|
44
|
+
} else {
|
|
45
|
+
lastOrder = order;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
QtiSetOutcomeValue,
|
|
8
8
|
QtiTemplateRule,
|
|
9
9
|
} from "./types.js";
|
|
10
|
+
import { parseXmlSchemaRegex, patternVariableIdentifier } from "./xml-schema-regex.js";
|
|
10
11
|
import { expressionChildren } from "./processing-expression-children.js";
|
|
11
12
|
import { MAX_QTI_REPEAT_RESULT_ELEMENTS } from "./processing-limits.js";
|
|
12
13
|
import { MATH_OPERATOR_NAMES, STATS_OPERATOR_NAMES } from "./processing-operators.js";
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
isInteger,
|
|
22
23
|
} from "./validation-primitives.js";
|
|
23
24
|
import { COMPLETION_STATUS } from "./attempt-state-constants.js";
|
|
25
|
+
import { validateEqualTolerance } from "./processing-equality.js";
|
|
24
26
|
|
|
25
27
|
export function validateResponseProcessingTemplate(
|
|
26
28
|
item: QtiAssessmentItem,
|
|
@@ -120,7 +122,14 @@ export function validateProcessingReferences(
|
|
|
120
122
|
const outcomes = new Set(item.outcomeDeclarations.map((declaration) => declaration.identifier));
|
|
121
123
|
outcomes.add(COMPLETION_STATUS);
|
|
122
124
|
const templates = new Set(item.templateDeclarations.map((declaration) => declaration.identifier));
|
|
123
|
-
const variables = new Set([
|
|
125
|
+
const variables = new Set([
|
|
126
|
+
...responses,
|
|
127
|
+
...outcomes,
|
|
128
|
+
...templates,
|
|
129
|
+
"numAttempts",
|
|
130
|
+
"QTI_CONTEXT",
|
|
131
|
+
...(item.timeDependent ? ["duration"] : []),
|
|
132
|
+
]);
|
|
124
133
|
|
|
125
134
|
for (const rule of item.templateProcessing?.rules ?? []) {
|
|
126
135
|
validateTemplateRule(rule, responses, outcomes, templates, variables, diagnostics);
|
|
@@ -324,7 +333,7 @@ function validateExpressionReferences(
|
|
|
324
333
|
): void {
|
|
325
334
|
if (!expression) return;
|
|
326
335
|
|
|
327
|
-
if (expression.type === "variable"
|
|
336
|
+
if (expression.type === "variable") {
|
|
328
337
|
validateProcessingIdentifier(
|
|
329
338
|
expression.identifier,
|
|
330
339
|
"processing.variable",
|
|
@@ -429,6 +438,30 @@ function validateExpressionReferences(
|
|
|
429
438
|
}
|
|
430
439
|
}
|
|
431
440
|
|
|
441
|
+
if (expression.type === "patternMatch") {
|
|
442
|
+
const reference = patternVariableIdentifier(expression.pattern);
|
|
443
|
+
if (reference !== undefined) {
|
|
444
|
+
if (!variables.has(reference))
|
|
445
|
+
diagnostics.push({
|
|
446
|
+
code: "processing.variable.reference",
|
|
447
|
+
severity: "error",
|
|
448
|
+
message: `Pattern references missing variable ${reference}.`,
|
|
449
|
+
source: expression.source,
|
|
450
|
+
path: expression.source?.path,
|
|
451
|
+
});
|
|
452
|
+
} else {
|
|
453
|
+
const parsed = parseXmlSchemaRegex(expression.pattern);
|
|
454
|
+
if (!parsed.ok)
|
|
455
|
+
diagnostics.push({
|
|
456
|
+
code: `processing.pattern.${parsed.problem.code}`,
|
|
457
|
+
severity: "error",
|
|
458
|
+
message: parsed.problem.message,
|
|
459
|
+
source: expression.source,
|
|
460
|
+
path: expression.source?.path,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
432
465
|
if (expression.type === "randomInteger") {
|
|
433
466
|
validateRandomIntegerExpression(expression, diagnostics);
|
|
434
467
|
}
|
|
@@ -446,11 +479,16 @@ function validateExpressionReferences(
|
|
|
446
479
|
"qti-equal-rounded",
|
|
447
480
|
expression.roundingMode,
|
|
448
481
|
expression.figures,
|
|
482
|
+
variables,
|
|
449
483
|
diagnostics,
|
|
450
484
|
expression.source,
|
|
451
485
|
);
|
|
452
486
|
}
|
|
453
487
|
|
|
488
|
+
if (expression.type === "equal") {
|
|
489
|
+
validateEqualTolerance(expression, variables, diagnostics);
|
|
490
|
+
}
|
|
491
|
+
|
|
454
492
|
if (expression.type === "anyN") {
|
|
455
493
|
validateAnyNBound("min", expression.min, responses, variables, diagnostics, expression.source);
|
|
456
494
|
validateAnyNBound("max", expression.max, responses, variables, diagnostics, expression.source);
|
|
@@ -544,7 +582,8 @@ const statsOperatorNames = new Set<string>(STATS_OPERATOR_NAMES);
|
|
|
544
582
|
function validateRounding(
|
|
545
583
|
qtiName: string,
|
|
546
584
|
roundingMode: string,
|
|
547
|
-
figures: number,
|
|
585
|
+
figures: number | string,
|
|
586
|
+
variables: ReadonlySet<string>,
|
|
548
587
|
diagnostics: QtiDiagnostic[],
|
|
549
588
|
source: QtiDiagnostic["source"],
|
|
550
589
|
): void {
|
|
@@ -558,7 +597,10 @@ function validateRounding(
|
|
|
558
597
|
});
|
|
559
598
|
}
|
|
560
599
|
const validFigures =
|
|
561
|
-
|
|
600
|
+
typeof figures === "string"
|
|
601
|
+
? variables.has(figures)
|
|
602
|
+
: Number.isInteger(figures) &&
|
|
603
|
+
(roundingMode === "decimalPlaces" ? figures >= 0 : figures > 0);
|
|
562
604
|
if (!validFigures) {
|
|
563
605
|
diagnostics.push({
|
|
564
606
|
code: "processing.roundingFigures",
|
|
@@ -683,12 +725,7 @@ function minimumExpressionElements(expression: QtiProcessingExpression): number
|
|
|
683
725
|
return minimumProducedElements(expression.expressions);
|
|
684
726
|
}
|
|
685
727
|
if (expression.type === "random") {
|
|
686
|
-
|
|
687
|
-
let minimum = MAX_QTI_REPEAT_RESULT_ELEMENTS + 1;
|
|
688
|
-
for (const value of expression.values) {
|
|
689
|
-
minimum = Math.min(minimum, minimumExpressionElements(value));
|
|
690
|
-
}
|
|
691
|
-
return minimum;
|
|
728
|
+
return minimumExpressionElements(expression.expression) > 0 ? 1 : 0;
|
|
692
729
|
}
|
|
693
730
|
if (expression.type !== "repeat" || !/^\d+$/.test(expression.numberRepeats)) return 0;
|
|
694
731
|
const perRepeat = minimumProducedElements(expression.expressions);
|
package/src/validation.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { validateAssessmentItemChildren } from "./validation-item-children.js";
|
|
1
2
|
import type {
|
|
2
3
|
QtiAssessmentItem,
|
|
3
4
|
QtiBaseType,
|
|
@@ -44,6 +45,7 @@ export function validateAssessmentItem(document: QtiDocument): QtiValidationResu
|
|
|
44
45
|
|
|
45
46
|
requireIdentifier("qti-assessment-item", item.identifier, diagnostics, item.source);
|
|
46
47
|
validateAssessmentItemRoot(item, diagnostics);
|
|
48
|
+
validateAssessmentItemChildren(item, diagnostics);
|
|
47
49
|
validateItemBody(item, diagnostics);
|
|
48
50
|
validateItemBodySharedVocabulary(item, diagnostics);
|
|
49
51
|
validateDeclarationIdentifiers(item, diagnostics);
|
|
@@ -132,6 +134,18 @@ function validateDeclarationIdentifiers(
|
|
|
132
134
|
source: declaration.source,
|
|
133
135
|
});
|
|
134
136
|
}
|
|
137
|
+
if (
|
|
138
|
+
["numAttempts", "duration", "QTI_CONTEXT"].includes(declaration.identifier) ||
|
|
139
|
+
(declaration.identifier === COMPLETION_STATUS && declaration.kind !== "outcome")
|
|
140
|
+
) {
|
|
141
|
+
diagnostics.push({
|
|
142
|
+
code: "declaration.builtIn",
|
|
143
|
+
severity: "error",
|
|
144
|
+
message: `${declaration.identifier} is a reserved built-in variable and must not be declared explicitly.`,
|
|
145
|
+
path: declaration.source?.path,
|
|
146
|
+
source: declaration.source,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
135
149
|
if (seen.has(declaration.identifier)) {
|
|
136
150
|
diagnostics.push({
|
|
137
151
|
code: "identifier.duplicate",
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { QtiRecordValue, QtiValue } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Narrow an already-typed QTI value to its record variant. */
|
|
4
|
+
export function isRecordValue(value: QtiValue): value is QtiRecordValue {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6
|
+
}
|