@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/types.ts
CHANGED
|
@@ -250,6 +250,8 @@ export interface QtiGapMatchGapSegment {
|
|
|
250
250
|
}
|
|
251
251
|
|
|
252
252
|
export interface QtiObjectAsset {
|
|
253
|
+
/** Attributes of an authored image or picture fallback, when present. */
|
|
254
|
+
imageAttributes?: Record<string, string> | undefined;
|
|
253
255
|
data?: string | undefined;
|
|
254
256
|
type?: string | undefined;
|
|
255
257
|
width?: string | undefined;
|
|
@@ -457,6 +459,13 @@ export interface QtiAssessmentItem {
|
|
|
457
459
|
stylesheets: QtiStylesheet[];
|
|
458
460
|
body: QtiContentNode[];
|
|
459
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;
|
|
460
469
|
source?: QtiSourceLocation | undefined;
|
|
461
470
|
}
|
|
462
471
|
|
|
@@ -591,7 +600,7 @@ export type QtiProcessingExpression = (
|
|
|
591
600
|
baseType?: string | undefined;
|
|
592
601
|
}
|
|
593
602
|
| { type: "null" }
|
|
594
|
-
| { type: "isNull";
|
|
603
|
+
| { type: "isNull"; expression: QtiProcessingExpression }
|
|
595
604
|
| { type: "matchCorrect"; identifier: string; correctIdentifier: string }
|
|
596
605
|
| { type: "match"; left: QtiProcessingExpression; right: QtiProcessingExpression }
|
|
597
606
|
| { type: "correct"; identifier: string }
|
|
@@ -612,7 +621,7 @@ export type QtiProcessingExpression = (
|
|
|
612
621
|
max: number;
|
|
613
622
|
attributes: Record<string, string>;
|
|
614
623
|
}
|
|
615
|
-
| { type: "random";
|
|
624
|
+
| { type: "random"; expression: QtiProcessingExpression }
|
|
616
625
|
| { type: "multiple"; expressions: QtiProcessingExpression[] }
|
|
617
626
|
| { type: "ordered"; expressions: QtiProcessingExpression[] }
|
|
618
627
|
| { type: "index"; expression: QtiProcessingExpression; n: string }
|
|
@@ -639,13 +648,18 @@ export type QtiProcessingExpression = (
|
|
|
639
648
|
| { type: "anyN"; expressions: QtiProcessingExpression[]; min: string; max: string }
|
|
640
649
|
| { type: "or"; expressions: QtiProcessingExpression[] }
|
|
641
650
|
| { type: "not"; expression: QtiProcessingExpression }
|
|
642
|
-
| {
|
|
651
|
+
| {
|
|
652
|
+
type: "equal";
|
|
653
|
+
left: QtiProcessingExpression;
|
|
654
|
+
right: QtiProcessingExpression;
|
|
655
|
+
attributes?: Record<string, string> | undefined;
|
|
656
|
+
}
|
|
643
657
|
| {
|
|
644
658
|
type: "equalRounded";
|
|
645
659
|
left: QtiProcessingExpression;
|
|
646
660
|
right: QtiProcessingExpression;
|
|
647
661
|
roundingMode: string;
|
|
648
|
-
figures: number;
|
|
662
|
+
figures: number | string;
|
|
649
663
|
}
|
|
650
664
|
| {
|
|
651
665
|
type: "numericCompare";
|
|
@@ -722,8 +736,22 @@ export interface QtiValidationResult {
|
|
|
722
736
|
diagnostics: QtiDiagnostic[];
|
|
723
737
|
}
|
|
724
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
|
+
|
|
725
752
|
export interface QtiAttemptStateV1 {
|
|
726
753
|
schema: "qti3.attempt-state.v1";
|
|
754
|
+
builtInVariables?: QtiBuiltInVariables | undefined;
|
|
727
755
|
itemIdentifier: string;
|
|
728
756
|
status: QtiAttemptStatus;
|
|
729
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",
|
|
@@ -34,10 +34,13 @@ export function validateInteractions(item: QtiAssessmentItem, diagnostics: QtiDi
|
|
|
34
34
|
validateInteractionSharedVocabulary(interaction, diagnostics);
|
|
35
35
|
validateInteractionChoices(interaction, diagnostics);
|
|
36
36
|
validateInteractionChildren(interaction, diagnostics);
|
|
37
|
+
validateGraphicGapMatchChildren(interaction, diagnostics);
|
|
38
|
+
validatePositionObjectChildren(interaction, diagnostics);
|
|
37
39
|
validateInteractionRequiredAttributes(interaction, diagnostics);
|
|
38
40
|
validatePortableCustomInteraction(interaction, item, diagnostics);
|
|
39
41
|
validateInteractionLimitAttributes(interaction, diagnostics);
|
|
40
42
|
validatePatternMaskAttribute(interaction, diagnostics);
|
|
43
|
+
validateTextInteractionContract(interaction, responseDeclarations, diagnostics);
|
|
41
44
|
validateGraphicHotspotObjectDimensions(interaction, diagnostics);
|
|
42
45
|
validateCorrectResponseReferences(
|
|
43
46
|
interaction,
|
|
@@ -649,7 +652,7 @@ function allowedInteractionChildren(interaction: QtiInteraction): Set<string> |
|
|
|
649
652
|
case "order":
|
|
650
653
|
return setOf(common, ["qti-simple-choice"]);
|
|
651
654
|
case "associate":
|
|
652
|
-
return setOf(common, ["qti-simple-
|
|
655
|
+
return setOf(common, ["qti-simple-associable-choice"]);
|
|
653
656
|
case "match":
|
|
654
657
|
return setOf(common, ["qti-simple-match-set"]);
|
|
655
658
|
case "gapMatch":
|
|
@@ -659,22 +662,24 @@ function allowedInteractionChildren(interaction: QtiInteraction): Set<string> |
|
|
|
659
662
|
case "hottext":
|
|
660
663
|
return setOf(common, staticContentNames());
|
|
661
664
|
case "graphicOrder":
|
|
662
|
-
return setOf(common, ["object", "qti-hotspot-choice"]);
|
|
665
|
+
return setOf(common, ["object", "img", "picture", "qti-hotspot-choice"]);
|
|
663
666
|
case "graphicAssociate":
|
|
664
|
-
return setOf(common, ["object", "qti-associable-hotspot"]);
|
|
667
|
+
return setOf(common, ["object", "img", "picture", "qti-associable-hotspot"]);
|
|
665
668
|
case "graphicGapMatch":
|
|
666
669
|
return setOf(common, [
|
|
667
670
|
"object",
|
|
671
|
+
"img",
|
|
672
|
+
"picture",
|
|
668
673
|
"qti-gap-text",
|
|
669
674
|
"qti-gap-img",
|
|
670
675
|
"qti-associable-hotspot",
|
|
671
|
-
...staticContentNames(),
|
|
672
676
|
]);
|
|
673
677
|
case "hotspot":
|
|
674
|
-
return setOf(common, ["object", "qti-hotspot-choice"]);
|
|
678
|
+
return setOf(common, ["object", "img", "picture", "qti-hotspot-choice"]);
|
|
675
679
|
case "positionObject":
|
|
676
|
-
return
|
|
680
|
+
return new Set(["object", "img", "picture"]);
|
|
677
681
|
case "selectPoint":
|
|
682
|
+
return setOf(common, ["object", "img", "picture"]);
|
|
678
683
|
case "media":
|
|
679
684
|
return setOf(common, ["audio", "video", "object", "img"]);
|
|
680
685
|
case "drawing":
|
|
@@ -722,13 +727,12 @@ function expectedResponseShape(
|
|
|
722
727
|
return { cardinalities: ["ordered"], baseTypes: ["identifier"] };
|
|
723
728
|
}
|
|
724
729
|
if (interaction.type === "associate" || interaction.type === "graphicAssociate") {
|
|
725
|
-
return { cardinalities: ["multiple"], baseTypes: ["pair"
|
|
730
|
+
return { cardinalities: ["single", "multiple"], baseTypes: ["pair"] };
|
|
726
731
|
}
|
|
727
|
-
if (
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
) {
|
|
732
|
+
if (interaction.type === "match" || interaction.type === "gapMatch") {
|
|
733
|
+
return { cardinalities: ["single", "multiple"], baseTypes: ["directedPair"] };
|
|
734
|
+
}
|
|
735
|
+
if (interaction.type === "graphicGapMatch") {
|
|
732
736
|
return { cardinalities: ["multiple"], baseTypes: ["directedPair"] };
|
|
733
737
|
}
|
|
734
738
|
if (interaction.type === "selectPoint" || interaction.type === "positionObject") {
|
|
@@ -740,8 +744,14 @@ function expectedResponseShape(
|
|
|
740
744
|
if (interaction.type === "upload") {
|
|
741
745
|
return { cardinalities: ["single"], baseTypes: ["file"] };
|
|
742
746
|
}
|
|
743
|
-
if (interaction.type === "textEntry"
|
|
744
|
-
return { cardinalities: ["single"], baseTypes: ["string"] };
|
|
747
|
+
if (interaction.type === "textEntry") {
|
|
748
|
+
return { cardinalities: ["single", "record"], baseTypes: ["string", "integer", "float"] };
|
|
749
|
+
}
|
|
750
|
+
if (interaction.type === "extendedText") {
|
|
751
|
+
return {
|
|
752
|
+
cardinalities: ["single", "multiple", "ordered", "record"],
|
|
753
|
+
baseTypes: ["string", "integer", "float"],
|
|
754
|
+
};
|
|
745
755
|
}
|
|
746
756
|
if (interaction.type === "drawing") return { cardinalities: ["single"], baseTypes: ["file"] };
|
|
747
757
|
if (interaction.type === "portableCustom") {
|
|
@@ -780,3 +790,118 @@ function needsChoices(interaction: QtiInteraction): boolean {
|
|
|
780
790
|
interaction.type === "hotspot"
|
|
781
791
|
);
|
|
782
792
|
}
|
|
793
|
+
|
|
794
|
+
function validateTextInteractionContract(
|
|
795
|
+
interaction: QtiInteraction,
|
|
796
|
+
declarations: ReadonlyMap<string, QtiResponseDeclaration>,
|
|
797
|
+
diagnostics: QtiDiagnostic[],
|
|
798
|
+
): void {
|
|
799
|
+
if (interaction.type !== "textEntry" && interaction.type !== "extendedText") return;
|
|
800
|
+
const report = (code: string, message: string) =>
|
|
801
|
+
diagnostics.push({
|
|
802
|
+
code,
|
|
803
|
+
severity: "error",
|
|
804
|
+
message,
|
|
805
|
+
path: interaction.source?.path,
|
|
806
|
+
source: interaction.source,
|
|
807
|
+
});
|
|
808
|
+
const base = Number(interaction.attributes.base ?? 10);
|
|
809
|
+
if (!Number.isInteger(base) || base < 2 || base > 36) {
|
|
810
|
+
report("interaction.text.base", "Text response base must be an integer from 2 through 36.");
|
|
811
|
+
}
|
|
812
|
+
const format = interaction.attributes.format;
|
|
813
|
+
if (
|
|
814
|
+
format !== undefined &&
|
|
815
|
+
format !== "plain" &&
|
|
816
|
+
!(interaction.type === "extendedText" && format === "preformatted") &&
|
|
817
|
+
!(
|
|
818
|
+
interaction.type === "extendedText" &&
|
|
819
|
+
format === "xhtml" &&
|
|
820
|
+
interaction.responseBaseType === "string"
|
|
821
|
+
)
|
|
822
|
+
) {
|
|
823
|
+
report(
|
|
824
|
+
"interaction.text.format.unsupported",
|
|
825
|
+
`Text format ${format} is not supported; use plain text.`,
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
if (interaction.type === "extendedText") {
|
|
829
|
+
const container =
|
|
830
|
+
interaction.responseCardinality === "multiple" ||
|
|
831
|
+
interaction.responseCardinality === "ordered";
|
|
832
|
+
const max = interaction.attributes["max-strings"];
|
|
833
|
+
const min = interaction.attributes["min-strings"] ?? "0";
|
|
834
|
+
if (
|
|
835
|
+
(container && max === undefined) ||
|
|
836
|
+
(max !== undefined && !isNonNegativeInteger(max)) ||
|
|
837
|
+
!isNonNegativeInteger(min) ||
|
|
838
|
+
Number(min) > (container ? Number(max) : 1)
|
|
839
|
+
) {
|
|
840
|
+
report(
|
|
841
|
+
"interaction.text.strings",
|
|
842
|
+
"Extended text requires valid min-strings/max-strings limits; containers require max-strings and the minimum must not exceed the maximum.",
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
const companion = interaction.attributes["string-identifier"];
|
|
847
|
+
if (companion !== undefined) {
|
|
848
|
+
const declaration = declarations.get(companion);
|
|
849
|
+
if (
|
|
850
|
+
!declaration ||
|
|
851
|
+
declaration.baseType !== "string" ||
|
|
852
|
+
declaration.cardinality !== interaction.responseCardinality ||
|
|
853
|
+
companion === interaction.responseIdentifier ||
|
|
854
|
+
(interaction.responseBaseType !== "integer" && interaction.responseBaseType !== "float")
|
|
855
|
+
) {
|
|
856
|
+
report(
|
|
857
|
+
"interaction.text.stringIdentifier",
|
|
858
|
+
"string-identifier must reference a separate string response with matching cardinality for a numeric text interaction.",
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function validateGraphicGapMatchChildren(
|
|
865
|
+
interaction: QtiInteraction,
|
|
866
|
+
diagnostics: QtiDiagnostic[],
|
|
867
|
+
): void {
|
|
868
|
+
if (interaction.type !== "graphicGapMatch") return;
|
|
869
|
+
const names = interaction.childElements.map((child) => child.qtiName);
|
|
870
|
+
const sequence = names.join(" ");
|
|
871
|
+
if (
|
|
872
|
+
/^(?:qti-prompt )?(?:object|img|picture)(?: qti-gap-(?:text|img))+(?: qti-associable-hotspot)+$/.test(
|
|
873
|
+
sequence,
|
|
874
|
+
)
|
|
875
|
+
)
|
|
876
|
+
return;
|
|
877
|
+
diagnostics.push({
|
|
878
|
+
code: "interaction.graphicGapMatch.children",
|
|
879
|
+
severity: "error",
|
|
880
|
+
message:
|
|
881
|
+
"Graphic Gap Match requires an optional prompt, one image, gap choices, then one or more associable hotspots, in that order.",
|
|
882
|
+
path: interaction.source?.path,
|
|
883
|
+
source: interaction.source,
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
function validatePositionObjectChildren(
|
|
888
|
+
interaction: QtiInteraction,
|
|
889
|
+
diagnostics: QtiDiagnostic[],
|
|
890
|
+
): void {
|
|
891
|
+
if (interaction.type !== "positionObject") return;
|
|
892
|
+
const [child] = interaction.childElements;
|
|
893
|
+
if (
|
|
894
|
+
interaction.childElements.length === 1 &&
|
|
895
|
+
child &&
|
|
896
|
+
["object", "img", "picture"].includes(child.qtiName)
|
|
897
|
+
)
|
|
898
|
+
return;
|
|
899
|
+
diagnostics.push({
|
|
900
|
+
code: "interaction.positionObject.children",
|
|
901
|
+
severity: "error",
|
|
902
|
+
message:
|
|
903
|
+
"Position Object requires exactly one marker image (object, img, or picture); instructions belong before its stage.",
|
|
904
|
+
path: interaction.source?.path,
|
|
905
|
+
source: interaction.source,
|
|
906
|
+
});
|
|
907
|
+
}
|
|
@@ -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
|
+
}
|