@longsightgroup/qti3-core 0.10.5 → 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 +153 -4
- 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/catalog.d.ts +8 -0
- package/dist/catalog.d.ts.map +1 -1
- package/dist/catalog.js +10 -0
- package/dist/catalog.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 +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/item-scoring-expectation.d.ts +2 -0
- package/dist/item-scoring-expectation.d.ts.map +1 -1
- package/dist/item-scoring-expectation.js +8 -0
- package/dist/item-scoring-expectation.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 +36 -6
- 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 +3 -1
- package/dist/response-validation-policy.d.ts.map +1 -1
- package/dist/response-validation-policy.js +66 -6
- package/dist/response-validation-policy.js.map +1 -1
- package/dist/response-validation.d.ts +3 -1
- package/dist/response-validation.d.ts.map +1 -1
- package/dist/response-validation.js +32 -10
- package/dist/response-validation.js.map +1 -1
- package/dist/scoring-disposition-policy.d.ts.map +1 -1
- package/dist/scoring-disposition-policy.js +6 -2
- package/dist/scoring-disposition-policy.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 +16 -3
- package/dist/support-evidence.js.map +1 -1
- package/dist/support.d.ts.map +1 -1
- package/dist/support.js +72 -24
- package/dist/support.js.map +1 -1
- package/dist/text-response.d.ts +9 -0
- package/dist/text-response.d.ts.map +1 -0
- package/dist/text-response.js +102 -0
- package/dist/text-response.js.map +1 -0
- 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 +12 -6
- package/dist/trusted-item-session.js.map +1 -1
- package/dist/types.d.ts +23 -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-interactions.d.ts.map +1 -1
- package/dist/validation-interactions.js +102 -12
- package/dist/validation-interactions.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/catalog.ts +29 -0
- package/src/diagnostics.ts +29 -0
- package/src/index.ts +16 -0
- package/src/item-scoring-expectation.ts +8 -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 +38 -6
- 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 +76 -4
- package/src/response-validation.ts +51 -5
- package/src/scoring-disposition-policy.ts +7 -2
- 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 +16 -3
- package/src/support.ts +94 -24
- package/src/text-response.ts +106 -0
- package/src/trusted-item-session.ts +17 -5
- package/src/types.ts +32 -4
- package/src/validation-declaration-entries.ts +11 -1
- package/src/validation-interactions.ts +139 -14
- 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/session.ts
CHANGED
|
@@ -43,6 +43,9 @@ import {
|
|
|
43
43
|
} from "./processing-state.js";
|
|
44
44
|
import { booleanValue, normalizeValueForCardinality, qtiMatchValues } from "./processing-values.js";
|
|
45
45
|
|
|
46
|
+
import { createSessionBuiltIns, type QtiSessionEnvironment } from "./session-builtins.js";
|
|
47
|
+
export type { QtiSessionEnvironment } from "./session-builtins.js";
|
|
48
|
+
|
|
46
49
|
interface SessionProcessingContext {
|
|
47
50
|
responseDefaults: Record<string, QtiValue>;
|
|
48
51
|
evaluation: EvaluationContext;
|
|
@@ -58,7 +61,7 @@ interface ConditionalRules<Rule> {
|
|
|
58
61
|
elseRules: Rule[];
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
export interface QtiItemSessionOptions {
|
|
64
|
+
export interface QtiItemSessionOptions extends QtiSessionEnvironment {
|
|
62
65
|
randomSeed?: string | number | undefined;
|
|
63
66
|
customOperators?: QtiCustomOperatorRegistry | undefined;
|
|
64
67
|
allowedUndeclaredResponseIdentifiers?: readonly string[] | undefined;
|
|
@@ -67,6 +70,8 @@ export interface QtiItemSessionOptions {
|
|
|
67
70
|
export interface QtiItemSession {
|
|
68
71
|
readonly item: QtiAssessmentItem;
|
|
69
72
|
correctResponses(): Record<string, QtiValue>;
|
|
73
|
+
/** Starts an attempt without requiring a response; repeated calls and resume are idempotent. */
|
|
74
|
+
beginAttempt(): void;
|
|
70
75
|
respond(identifier: string, value: QtiValue): void;
|
|
71
76
|
setInteractionState(identifier: string, state: QtiPortableCustomStateValue): void;
|
|
72
77
|
interactionState(identifier: string): QtiPortableCustomStateValue | undefined;
|
|
@@ -80,11 +85,12 @@ export function visibleModalFeedback(
|
|
|
80
85
|
outcomes: Record<string, QtiValue>,
|
|
81
86
|
): QtiModalFeedback[] {
|
|
82
87
|
return item.modalFeedback.filter((feedback) => {
|
|
83
|
-
if (feedback.showHide === "hide") return false;
|
|
84
88
|
const outcome = outcomes[feedback.outcomeIdentifier];
|
|
85
89
|
const outcomeValue: QtiValue = outcome === undefined ? null : outcome;
|
|
86
|
-
|
|
87
|
-
|
|
90
|
+
const matches = Array.isArray(outcomeValue)
|
|
91
|
+
? outcomeValue.includes(feedback.identifier)
|
|
92
|
+
: qtiValueToString(outcomeValue) === feedback.identifier;
|
|
93
|
+
return feedback.showHide === "hide" ? !matches : matches;
|
|
88
94
|
});
|
|
89
95
|
}
|
|
90
96
|
|
|
@@ -109,6 +115,12 @@ export function createItemSession(
|
|
|
109
115
|
const interactionStates: Record<string, QtiPortableCustomStateValue> = {};
|
|
110
116
|
const correctResponses: Record<string, QtiValue> = {};
|
|
111
117
|
let status: QtiAttemptStatus = priorState?.status ?? "initialized";
|
|
118
|
+
const builtInDiagnostics: QtiDiagnostic[] = [];
|
|
119
|
+
const builtIns = createSessionBuiltIns(priorState?.builtInVariables, options, (diagnostic) => {
|
|
120
|
+
if (!builtInDiagnostics.some((existing) => existing.code === diagnostic.code))
|
|
121
|
+
builtInDiagnostics.push(diagnostic);
|
|
122
|
+
});
|
|
123
|
+
const captureTime = () => builtIns.captureTime(status !== "suspended" && status !== "completed");
|
|
112
124
|
const random = seededRandom(options.randomSeed ?? document.item.identifier);
|
|
113
125
|
const customOperators = options.customOperators ?? {};
|
|
114
126
|
const allowedUndeclaredResponseIdentifiers = new Set(
|
|
@@ -131,7 +143,10 @@ export function createItemSession(
|
|
|
131
143
|
templateValues[declaration.identifier] = cloneValue(declaration.defaultValue);
|
|
132
144
|
}
|
|
133
145
|
for (const outcome of document.item.outcomeDeclarations) {
|
|
134
|
-
|
|
146
|
+
const numericSingle =
|
|
147
|
+
outcome.cardinality === "single" &&
|
|
148
|
+
(outcome.baseType === "integer" || outcome.baseType === "float");
|
|
149
|
+
outcomes[outcome.identifier] = cloneValue(outcome.defaultValue ?? (numericSingle ? 0 : null));
|
|
135
150
|
}
|
|
136
151
|
outcomes[COMPLETION_STATUS] = COMPLETION_NOT_ATTEMPTED;
|
|
137
152
|
const baseResponses = cloneValueRecord(responses);
|
|
@@ -145,7 +160,20 @@ export function createItemSession(
|
|
|
145
160
|
correctResponses,
|
|
146
161
|
random,
|
|
147
162
|
customOperators,
|
|
148
|
-
{
|
|
163
|
+
{
|
|
164
|
+
allowedUndeclaredResponseIdentifiers,
|
|
165
|
+
builtInVariable(identifier) {
|
|
166
|
+
if (identifier === "completionStatus")
|
|
167
|
+
return outcomes[COMPLETION_STATUS] ?? COMPLETION_NOT_ATTEMPTED;
|
|
168
|
+
if (identifier === "numAttempts") return builtIns.state.numAttempts;
|
|
169
|
+
if (identifier === "QTI_CONTEXT") return { ...builtIns.state.context };
|
|
170
|
+
if (identifier === "duration" && document.item.timeDependent) {
|
|
171
|
+
captureTime();
|
|
172
|
+
return builtIns.duration();
|
|
173
|
+
}
|
|
174
|
+
return undefined;
|
|
175
|
+
},
|
|
176
|
+
},
|
|
149
177
|
);
|
|
150
178
|
const processingContext: SessionProcessingContext = {
|
|
151
179
|
responseDefaults,
|
|
@@ -170,6 +198,7 @@ export function createItemSession(
|
|
|
170
198
|
baseOutcomes,
|
|
171
199
|
);
|
|
172
200
|
}
|
|
201
|
+
const templateDiagnostics = [...evaluation.diagnostics, ...builtInDiagnostics];
|
|
173
202
|
const defaultOutcomes = cloneValueRecord(outcomes);
|
|
174
203
|
Object.assign(responses, priorResponses);
|
|
175
204
|
Object.assign(outcomes, priorOutcomes);
|
|
@@ -177,6 +206,7 @@ export function createItemSession(
|
|
|
177
206
|
|
|
178
207
|
return {
|
|
179
208
|
item: document.item,
|
|
209
|
+
beginAttempt: startAttempt,
|
|
180
210
|
correctResponses() {
|
|
181
211
|
return cloneValueRecord(correctResponses);
|
|
182
212
|
},
|
|
@@ -198,10 +228,15 @@ export function createItemSession(
|
|
|
198
228
|
return state === undefined ? undefined : clonePortableCustomState(state);
|
|
199
229
|
},
|
|
200
230
|
setStatus(nextStatus: QtiAttemptStatus) {
|
|
231
|
+
captureTime();
|
|
232
|
+
if (nextStatus === "interacting") startAttempt();
|
|
233
|
+
if (nextStatus === "completed") builtIns.state.attemptInProgress = false;
|
|
201
234
|
status = nextStatus;
|
|
202
235
|
},
|
|
203
236
|
score() {
|
|
204
|
-
|
|
237
|
+
evaluation.diagnostics.length = 0;
|
|
238
|
+
builtInDiagnostics.length = 0;
|
|
239
|
+
captureTime();
|
|
205
240
|
if (document.item.adaptive || status !== "initialized") {
|
|
206
241
|
startAttempt();
|
|
207
242
|
}
|
|
@@ -211,6 +246,12 @@ export function createItemSession(
|
|
|
211
246
|
outcomes[COMPLETION_STATUS] = completionStatus;
|
|
212
247
|
}
|
|
213
248
|
applyResponseProcessing(processingContext);
|
|
249
|
+
builtIns.state.attemptInProgress = false;
|
|
250
|
+
const diagnostics = [
|
|
251
|
+
...templateDiagnostics,
|
|
252
|
+
...evaluation.diagnostics,
|
|
253
|
+
...builtInDiagnostics,
|
|
254
|
+
];
|
|
214
255
|
if (outcomes[COMPLETION_STATUS] === COMPLETION_COMPLETED) status = "completed";
|
|
215
256
|
validationMessages = diagnostics;
|
|
216
257
|
const state = serialize(
|
|
@@ -221,10 +262,12 @@ export function createItemSession(
|
|
|
221
262
|
templateValues,
|
|
222
263
|
interactionStates,
|
|
223
264
|
diagnostics,
|
|
265
|
+
builtIns.state,
|
|
224
266
|
);
|
|
225
267
|
return { outcomes: cloneValueRecord(outcomes), diagnostics, state };
|
|
226
268
|
},
|
|
227
269
|
serialize() {
|
|
270
|
+
captureTime();
|
|
228
271
|
return serialize(
|
|
229
272
|
document.item.identifier,
|
|
230
273
|
status,
|
|
@@ -233,11 +276,18 @@ export function createItemSession(
|
|
|
233
276
|
templateValues,
|
|
234
277
|
interactionStates,
|
|
235
278
|
validationMessages,
|
|
279
|
+
builtIns.state,
|
|
236
280
|
);
|
|
237
281
|
},
|
|
238
282
|
};
|
|
239
283
|
|
|
240
284
|
function startAttempt(): void {
|
|
285
|
+
captureTime();
|
|
286
|
+
if (status === "completed") return;
|
|
287
|
+
if (!builtIns.state.attemptInProgress) {
|
|
288
|
+
builtIns.state.numAttempts += 1;
|
|
289
|
+
builtIns.state.attemptInProgress = true;
|
|
290
|
+
}
|
|
241
291
|
for (const [identifier, value] of Object.entries(responseDefaults)) {
|
|
242
292
|
if (responses[identifier] === undefined) responses[identifier] = cloneValue(value);
|
|
243
293
|
}
|
|
@@ -264,6 +314,8 @@ function applyTemplateProcessing(
|
|
|
264
314
|
): void {
|
|
265
315
|
const { evaluation } = context;
|
|
266
316
|
const rules = evaluation.document.item.templateProcessing?.rules ?? [];
|
|
317
|
+
// A retry or restored clone must derive defaults afresh from the authored declarations.
|
|
318
|
+
resetRecord(evaluation.defaultValues, {});
|
|
267
319
|
let restarts = 0;
|
|
268
320
|
for (let index = 0; index < rules.length; index += 1) {
|
|
269
321
|
const rule = rules[index]!;
|
|
@@ -276,6 +328,7 @@ function applyTemplateProcessing(
|
|
|
276
328
|
resetRecord(evaluation.responses, cloneValueRecord(baseResponses));
|
|
277
329
|
resetRecord(context.responseDefaults, cloneValueRecord(baseResponseDefaults));
|
|
278
330
|
resetRecord(evaluation.outcomes, cloneValueRecord(baseOutcomes));
|
|
331
|
+
resetRecord(evaluation.defaultValues, {});
|
|
279
332
|
resetCorrectResponses(evaluation.document, evaluation.correctResponses);
|
|
280
333
|
restarts += 1;
|
|
281
334
|
if (restarts <= 100) index = -1;
|
|
@@ -352,6 +405,7 @@ function applyTemplateRule(
|
|
|
352
405
|
const responseDeclaration = getResponseDeclaration(evaluation.document, rule.identifier);
|
|
353
406
|
if (responseDeclaration) {
|
|
354
407
|
const normalized = normalizeValueForCardinality(value, responseDeclaration.cardinality);
|
|
408
|
+
evaluation.defaultValues[rule.identifier] = cloneValue(normalized);
|
|
355
409
|
if (normalized === null) {
|
|
356
410
|
delete context.responseDefaults[rule.identifier];
|
|
357
411
|
} else {
|
|
@@ -363,7 +417,9 @@ function applyTemplateRule(
|
|
|
363
417
|
(declaration) => declaration.identifier === rule.identifier,
|
|
364
418
|
);
|
|
365
419
|
if (outcomeDeclaration) {
|
|
366
|
-
|
|
420
|
+
const normalized = normalizeValueForCardinality(value, outcomeDeclaration.cardinality);
|
|
421
|
+
evaluation.defaultValues[rule.identifier] = cloneValue(normalized);
|
|
422
|
+
evaluation.outcomes[rule.identifier] = cloneValue(normalized);
|
|
367
423
|
}
|
|
368
424
|
return false;
|
|
369
425
|
}
|
package/src/support-evidence.ts
CHANGED
|
@@ -51,21 +51,31 @@ export function browserTestsFor(interactionType: QtiInteractionType): string[] {
|
|
|
51
51
|
"tests/browser/player-interaction-sweep.spec.ts",
|
|
52
52
|
];
|
|
53
53
|
const extras: Partial<Record<QtiInteractionType, string[]>> = {
|
|
54
|
-
associate:
|
|
54
|
+
associate: [
|
|
55
|
+
...browserKeyboardA11yTests,
|
|
56
|
+
"packages/core/src/association-contracts.test.ts",
|
|
57
|
+
"tests/browser/player-dom-behavior.spec.ts",
|
|
58
|
+
],
|
|
55
59
|
choice: ["tests/browser/player-choice.spec.ts", "tests/browser/player-dom-behavior.spec.ts"],
|
|
56
60
|
drawing: ["tests/browser/player-graphic.spec.ts"],
|
|
57
61
|
endAttempt: ["tests/browser/player-dom-behavior.spec.ts", ...browserKeyboardA11yTests],
|
|
58
62
|
extendedText: [
|
|
63
|
+
"packages/core/src/text-response.test.ts",
|
|
59
64
|
"tests/browser/player-dom-behavior.spec.ts",
|
|
60
65
|
"tests/browser/player-extended-text-xhtml.spec.ts",
|
|
61
66
|
],
|
|
62
67
|
gapMatch: ["tests/browser/player-gap-match.spec.ts"],
|
|
63
68
|
graphicAssociate: ["tests/browser/player-graphic.spec.ts"],
|
|
64
69
|
graphicGapMatch: [
|
|
70
|
+
"packages/core/src/core-session-graphic-gap-state.test.ts",
|
|
65
71
|
"tests/browser/player-graphic-gap-match.spec.ts",
|
|
66
72
|
"tests/browser/player-graphic.spec.ts",
|
|
73
|
+
...browserLifecycleTests,
|
|
74
|
+
],
|
|
75
|
+
graphicOrder: [
|
|
76
|
+
"packages/core/src/graphic-image-contracts.test.ts",
|
|
77
|
+
"tests/browser/player-graphic.spec.ts",
|
|
67
78
|
],
|
|
68
|
-
graphicOrder: ["tests/browser/player-graphic.spec.ts"],
|
|
69
79
|
hotspot: ["tests/browser/player-graphic.spec.ts"],
|
|
70
80
|
hottext: ["tests/browser/player-hottext.spec.ts", "tests/browser/player-dom-behavior.spec.ts"],
|
|
71
81
|
inlineChoice: ["tests/browser/player-inline-choice.spec.ts"],
|
|
@@ -81,7 +91,10 @@ export function browserTestsFor(interactionType: QtiInteractionType): string[] {
|
|
|
81
91
|
"tests/browser/player-slider-cross-browser.spec.ts",
|
|
82
92
|
...browserKeyboardA11yTests,
|
|
83
93
|
],
|
|
84
|
-
textEntry: [
|
|
94
|
+
textEntry: [
|
|
95
|
+
"packages/core/src/text-response.test.ts",
|
|
96
|
+
"tests/browser/player-dom-behavior.spec.ts",
|
|
97
|
+
],
|
|
85
98
|
upload: ["tests/browser/player-dom-behavior.spec.ts"],
|
|
86
99
|
};
|
|
87
100
|
return [...base, ...(extras[interactionType] ?? [])];
|
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",
|
|
@@ -401,20 +462,29 @@ function entry(qtiName: string, interactionType: QtiInteractionType): QtiInterac
|
|
|
401
462
|
process: true,
|
|
402
463
|
fixtures: interactionSupportFixtures(interactionType),
|
|
403
464
|
tests: interactionSupportTests(interactionType),
|
|
465
|
+
notes: ["associate", "graphicAssociate", "match", "gapMatch"].includes(interactionType)
|
|
466
|
+
? interactionType === "associate" || interactionType === "graphicAssociate"
|
|
467
|
+
? "Supports single and multiple unordered pair responses."
|
|
468
|
+
: "Supports single and multiple directedPair responses."
|
|
469
|
+
: interactionType === "graphicGapMatch"
|
|
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."
|
|
471
|
+
: undefined,
|
|
404
472
|
};
|
|
405
473
|
}
|
|
406
474
|
|
|
407
475
|
function extendedTextInteractionEntry(): QtiInteractionElementSupport {
|
|
408
476
|
return {
|
|
409
477
|
...entry("qti-extended-text-interaction", "extendedText"),
|
|
410
|
-
notes:
|
|
478
|
+
notes:
|
|
479
|
+
"Supports single, multiple, ordered and numeric record responses; string/integer/float values; min-strings/max-strings; plain, preformatted and string XHTML capture.",
|
|
411
480
|
};
|
|
412
481
|
}
|
|
413
482
|
|
|
414
483
|
function textEntryInteractionEntry(): QtiInteractionElementSupport {
|
|
415
484
|
return {
|
|
416
485
|
...entry("qti-text-entry-interaction", "textEntry"),
|
|
417
|
-
notes:
|
|
486
|
+
notes:
|
|
487
|
+
"Supports single string/integer/float and numeric record responses, base 2–36, string-identifier, placeholder-text and pattern-mask; text-entry format is plain only.",
|
|
418
488
|
};
|
|
419
489
|
}
|
|
420
490
|
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { QtiInteraction, QtiRecordValue, QtiValue } from "./types.js";
|
|
2
|
+
|
|
3
|
+
/** Capture text using the interaction's declared response type and authored radix. */
|
|
4
|
+
export function captureQtiTextResponse(interaction: QtiInteraction, text: string): QtiValue {
|
|
5
|
+
if (interaction.responseCardinality === "record") return numericRecord(text, radix(interaction));
|
|
6
|
+
if (interaction.responseBaseType === "string") return text;
|
|
7
|
+
if (text.trim() === "") return null;
|
|
8
|
+
const record = numericRecord(text, radix(interaction));
|
|
9
|
+
return interaction.responseBaseType === "integer"
|
|
10
|
+
? (record.integerValue ?? null)
|
|
11
|
+
: (record.floatValue ?? null);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Recover editable text, including the original lexical form in a numeric record. */
|
|
15
|
+
export function qtiTextResponseString(value: QtiValue): string {
|
|
16
|
+
if (value !== null && typeof value === "object") {
|
|
17
|
+
return !Array.isArray(value) && typeof value.stringValue === "string" ? value.stringValue : "";
|
|
18
|
+
}
|
|
19
|
+
return value === null ? "" : String(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Format a captured scalar in its authored radix, preserving companion and record text. */
|
|
23
|
+
export function formatQtiTextResponse(interaction: QtiInteraction, value: QtiValue): string {
|
|
24
|
+
const base = radix(interaction);
|
|
25
|
+
if (typeof value === "number" && Number.isInteger(base) && base >= 2 && base <= 36) {
|
|
26
|
+
return value.toString(base);
|
|
27
|
+
}
|
|
28
|
+
return qtiTextResponseString(value);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function radix(interaction: QtiInteraction): number {
|
|
32
|
+
return Number(interaction.attributes.base ?? 10);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function numericRecord(text: string, base: number): QtiRecordValue {
|
|
36
|
+
const empty: QtiRecordValue = {
|
|
37
|
+
stringValue: text,
|
|
38
|
+
floatValue: null,
|
|
39
|
+
integerValue: null,
|
|
40
|
+
leftDigits: null,
|
|
41
|
+
rightDigits: null,
|
|
42
|
+
ndp: null,
|
|
43
|
+
nsf: null,
|
|
44
|
+
exponent: null,
|
|
45
|
+
};
|
|
46
|
+
if (!Number.isInteger(base) || base < 2 || base > 36) return empty;
|
|
47
|
+
const normalized = text.trim();
|
|
48
|
+
const match = (
|
|
49
|
+
base === 10
|
|
50
|
+
? /^([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/
|
|
51
|
+
: /^([+-]?)([0-9a-z]*)(?:\.([0-9a-z]*))?$/i
|
|
52
|
+
).exec(normalized);
|
|
53
|
+
if (!match) return empty;
|
|
54
|
+
const left = match[2] ?? "";
|
|
55
|
+
const right = match[3] ?? "";
|
|
56
|
+
const digits = left + right;
|
|
57
|
+
if (!digits || digits.split("").some((digit) => Number.parseInt(digit, 36) >= base)) return empty;
|
|
58
|
+
const exponent = match[4] === undefined ? null : Number(match[4]);
|
|
59
|
+
if (exponent !== null && !Number.isSafeInteger(exponent)) return empty;
|
|
60
|
+
const ndp = Math.max(0, right.length - (exponent ?? 0));
|
|
61
|
+
if (!Number.isSafeInteger(ndp)) return empty;
|
|
62
|
+
const magnitude =
|
|
63
|
+
(Number.parseInt(left || "0", base) +
|
|
64
|
+
right
|
|
65
|
+
.split("")
|
|
66
|
+
.reduce(
|
|
67
|
+
(sum, digit, index) => sum + Number.parseInt(digit, base) / base ** (index + 1),
|
|
68
|
+
0,
|
|
69
|
+
)) *
|
|
70
|
+
base ** (exponent ?? 0);
|
|
71
|
+
const value = base === 10 ? Number(normalized) : match[1] === "-" ? -magnitude : magnitude;
|
|
72
|
+
if (!Number.isFinite(value)) return empty;
|
|
73
|
+
const integer =
|
|
74
|
+
match[3] === undefined && exponent === null && Number.isSafeInteger(value) ? value : null;
|
|
75
|
+
return {
|
|
76
|
+
stringValue: text,
|
|
77
|
+
floatValue: value,
|
|
78
|
+
integerValue: integer,
|
|
79
|
+
leftDigits: left.length,
|
|
80
|
+
rightDigits: right.length,
|
|
81
|
+
ndp,
|
|
82
|
+
nsf: digits.replace(/^0+/, "").length || 1,
|
|
83
|
+
exponent,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isQtiTextResponseRecord(value: QtiValue): boolean {
|
|
88
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
89
|
+
const integerFields = new Set([
|
|
90
|
+
"integerValue",
|
|
91
|
+
"leftDigits",
|
|
92
|
+
"rightDigits",
|
|
93
|
+
"ndp",
|
|
94
|
+
"nsf",
|
|
95
|
+
"exponent",
|
|
96
|
+
]);
|
|
97
|
+
return Object.entries(value).every(([field, entry]) => {
|
|
98
|
+
if (field === "stringValue") return typeof entry === "string" || entry === null;
|
|
99
|
+
if (field === "floatValue")
|
|
100
|
+
return entry === null || (typeof entry === "number" && Number.isFinite(entry));
|
|
101
|
+
return (
|
|
102
|
+
integerFields.has(field) &&
|
|
103
|
+
(entry === null || (typeof entry === "number" && Number.isSafeInteger(entry)))
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import type { QtiSessionEnvironment } from "./session-builtins.js";
|
|
1
2
|
import { parseQtiXml } from "./parser.js";
|
|
2
|
-
import { itemExpectsAutomatedScore } from "./item-scoring-expectation.js";
|
|
3
|
+
import { itemExpectsAutomatedScore, itemHasExternalScore } from "./item-scoring-expectation.js";
|
|
3
4
|
import { parseQtiResponseVariables } from "./response-validation.js";
|
|
4
5
|
import { createItemSession, isQtiAttemptStateV1, type QtiItemSession } from "./session.js";
|
|
5
6
|
import type {
|
|
@@ -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,7 +195,11 @@ 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;
|
|
202
|
+
const externalScore = itemHasExternalScore(parsedResult.parsed.document.item);
|
|
193
203
|
|
|
194
204
|
let outcomes = sessionResult.session.serialize().outcomes;
|
|
195
205
|
let state = sessionResult.session.serialize();
|
|
@@ -212,7 +222,7 @@ export function runTrustedItemSession(
|
|
|
212
222
|
scoredResult.scored.state,
|
|
213
223
|
parsedResult.parsed.responseIdentifiers,
|
|
214
224
|
);
|
|
215
|
-
score = readNumericScore(outcomes.SCORE);
|
|
225
|
+
score = externalScore ? null : readNumericScore(outcomes.SCORE);
|
|
216
226
|
|
|
217
227
|
if (scoredResult.scored.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
218
228
|
return emptyTrustedItemSessionFailure(scoredDiagnostics, {
|
|
@@ -241,7 +251,7 @@ export function runTrustedItemSession(
|
|
|
241
251
|
} else {
|
|
242
252
|
state = stripUndeclaredResponses(state, parsedResult.parsed.responseIdentifiers);
|
|
243
253
|
outcomes = state.outcomes;
|
|
244
|
-
score = readNumericScore(outcomes.SCORE);
|
|
254
|
+
score = externalScore ? null : readNumericScore(outcomes.SCORE);
|
|
245
255
|
}
|
|
246
256
|
|
|
247
257
|
return {
|
|
@@ -276,11 +286,13 @@ function createTrustedItemSession(
|
|
|
276
286
|
diagnosticPrefix: QtiTrustedInputDiagnosticPrefix,
|
|
277
287
|
allowedUndeclaredResponseIdentifiers: readonly string[] | undefined,
|
|
278
288
|
existingDiagnostics: QtiDiagnostic[] = [],
|
|
289
|
+
sessionEnvironment: QtiSessionEnvironment = {},
|
|
279
290
|
): { ok: true; session: QtiItemSession } | { ok: false; diagnostics: QtiDiagnostic[] } {
|
|
280
291
|
try {
|
|
281
292
|
return {
|
|
282
293
|
ok: true,
|
|
283
294
|
session: createItemSession(parsed.document, priorState ?? undefined, {
|
|
295
|
+
...sessionEnvironment,
|
|
284
296
|
allowedUndeclaredResponseIdentifiers,
|
|
285
297
|
}),
|
|
286
298
|
};
|