@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.
- 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/binary-choice.d.ts +9 -0
- package/dist/binary-choice.d.ts.map +1 -0
- package/dist/binary-choice.js +81 -0
- package/dist/binary-choice.js.map +1 -0
- 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 +19 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -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-identifier.d.ts +3 -0
- package/dist/qti-identifier.d.ts.map +1 -0
- package/dist/qti-identifier.js +5 -0
- package/dist/qti-identifier.js.map +1 -0
- 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-paths.d.ts +2 -0
- package/dist/qti-package-paths.d.ts.map +1 -1
- package/dist/qti-package-paths.js +6 -0
- package/dist/qti-package-paths.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 +66 -23
- package/dist/support.js.map +1 -1
- package/dist/test-expression-parser.d.ts +6 -0
- package/dist/test-expression-parser.d.ts.map +1 -0
- package/dist/test-expression-parser.js +76 -0
- package/dist/test-expression-parser.js.map +1 -0
- package/dist/test-expression-serializer.d.ts +5 -0
- package/dist/test-expression-serializer.d.ts.map +1 -0
- package/dist/test-expression-serializer.js +54 -0
- package/dist/test-expression-serializer.js.map +1 -0
- package/dist/test-expression-validation.d.ts +14 -0
- package/dist/test-expression-validation.d.ts.map +1 -0
- package/dist/test-expression-validation.js +101 -0
- package/dist/test-expression-validation.js.map +1 -0
- package/dist/test-expression.d.ts +99 -0
- package/dist/test-expression.d.ts.map +1 -0
- package/dist/test-expression.js +28 -0
- package/dist/test-expression.js.map +1 -0
- package/dist/test-model.d.ts +62 -0
- package/dist/test-model.d.ts.map +1 -0
- package/dist/test-model.js +5 -0
- package/dist/test-model.js.map +1 -0
- package/dist/test-parser.d.ts +13 -0
- package/dist/test-parser.d.ts.map +1 -0
- package/dist/test-parser.js +144 -0
- package/dist/test-parser.js.map +1 -0
- package/dist/test-processing.d.ts +22 -0
- package/dist/test-processing.d.ts.map +1 -0
- package/dist/test-processing.js +84 -0
- package/dist/test-processing.js.map +1 -0
- package/dist/test-session.d.ts +31 -0
- package/dist/test-session.d.ts.map +1 -0
- package/dist/test-session.js +102 -0
- package/dist/test-session.js.map +1 -0
- package/dist/test-support.d.ts +4 -0
- package/dist/test-support.d.ts.map +1 -0
- package/dist/test-support.js +30 -0
- package/dist/test-support.js.map +1 -0
- package/dist/test-validation.d.ts +4 -0
- package/dist/test-validation.d.ts.map +1 -0
- package/dist/test-validation.js +133 -0
- package/dist/test-validation.js.map +1 -0
- package/dist/test-xml.d.ts +7 -0
- package/dist/test-xml.d.ts.map +1 -0
- package/dist/test-xml.js +23 -0
- package/dist/test-xml.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 +8 -3
- package/dist/trusted-item-session.js.map +1 -1
- package/dist/types.d.ts +26 -4
- 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/binary-choice.ts +96 -0
- package/src/diagnostics.ts +29 -0
- package/src/index.ts +38 -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-identifier.ts +4 -0
- 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-paths.ts +9 -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 +86 -23
- package/src/test-expression-parser.ts +89 -0
- package/src/test-expression-serializer.ts +71 -0
- package/src/test-expression-validation.ts +139 -0
- package/src/test-expression.ts +66 -0
- package/src/test-model.ts +68 -0
- package/src/test-parser.ts +201 -0
- package/src/test-processing.ts +107 -0
- package/src/test-session.ts +160 -0
- package/src/test-support.ts +31 -0
- package/src/test-validation.ts +190 -0
- package/src/test-xml.ts +37 -0
- package/src/trusted-item-session.ts +13 -2
- package/src/types.ts +36 -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/test-xml.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { QTI_ASI_NAMESPACE } from "./qti-namespaces.js";
|
|
2
|
+
import type { QtiDiagnostic } from "./types.js";
|
|
3
|
+
import type { XmlNode } from "./xml.js";
|
|
4
|
+
|
|
5
|
+
/** Report an unsupported construct at its XML source location. */
|
|
6
|
+
export function rejectTestXml(node: XmlNode, diagnostics: QtiDiagnostic[]): void {
|
|
7
|
+
diagnostics.push({
|
|
8
|
+
code: "test.xml.unsupported",
|
|
9
|
+
severity: "error",
|
|
10
|
+
message: `Unsupported test element or attributes: ${node.localName}.`,
|
|
11
|
+
source: node.source,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Check the closed test profile's namespace, attributes, children and element-only content. */
|
|
16
|
+
export function checkTestXml(
|
|
17
|
+
node: XmlNode,
|
|
18
|
+
names: readonly string[],
|
|
19
|
+
attrs: readonly string[],
|
|
20
|
+
diagnostics: QtiDiagnostic[],
|
|
21
|
+
allowText = false,
|
|
22
|
+
): void {
|
|
23
|
+
if (
|
|
24
|
+
node.uri !== QTI_ASI_NAMESPACE ||
|
|
25
|
+
Object.keys(node.attributes).some(
|
|
26
|
+
(name) =>
|
|
27
|
+
name !== "xmlns" &&
|
|
28
|
+
!name.startsWith("xmlns:") &&
|
|
29
|
+
!["xsi:schemaLocation", "xml:lang", ...attrs].includes(name),
|
|
30
|
+
) ||
|
|
31
|
+
(!allowText && node.text.trim().length > 0)
|
|
32
|
+
)
|
|
33
|
+
rejectTestXml(node, diagnostics);
|
|
34
|
+
for (const child of node.children)
|
|
35
|
+
if (child.uri !== QTI_ASI_NAMESPACE || !names.includes(child.localName))
|
|
36
|
+
rejectTestXml(child, diagnostics);
|
|
37
|
+
}
|
|
@@ -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>;
|
|
@@ -744,6 +770,7 @@ export interface QtiScoreResult {
|
|
|
744
770
|
export type QtiElementSupport =
|
|
745
771
|
| QtiInteractionElementSupport
|
|
746
772
|
| QtiProcessingElementSupport
|
|
773
|
+
| QtiTestElementSupport
|
|
747
774
|
| QtiItemMetadataElementSupport;
|
|
748
775
|
|
|
749
776
|
interface QtiElementSupportBase {
|
|
@@ -768,6 +795,11 @@ export interface QtiProcessingElementSupport extends QtiElementSupportBase {
|
|
|
768
795
|
category: "processing";
|
|
769
796
|
}
|
|
770
797
|
|
|
798
|
+
/** Executable assessment-test support, separate from item processing/rendering. */
|
|
799
|
+
export interface QtiTestElementSupport extends QtiElementSupportBase {
|
|
800
|
+
category: "test";
|
|
801
|
+
}
|
|
802
|
+
|
|
771
803
|
export interface QtiItemMetadataElementSupport extends QtiElementSupportBase {
|
|
772
804
|
category: "itemMetadata";
|
|
773
805
|
}
|
|
@@ -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
|
+
}
|