@longsightgroup/qti3-core 0.9.1 → 0.9.3
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 +59 -3
- package/dist/adaptive-turn-materializer.d.ts +14 -0
- package/dist/adaptive-turn-materializer.d.ts.map +1 -0
- package/dist/adaptive-turn-materializer.js +56 -0
- package/dist/adaptive-turn-materializer.js.map +1 -0
- package/dist/adaptive-turn.d.ts +23 -0
- package/dist/adaptive-turn.d.ts.map +1 -0
- package/dist/adaptive-turn.js +60 -0
- package/dist/adaptive-turn.js.map +1 -0
- package/dist/assert-never.d.ts +3 -0
- package/dist/assert-never.d.ts.map +1 -0
- package/dist/assert-never.js +5 -0
- package/dist/assert-never.js.map +1 -0
- package/dist/attempt-state-constants.d.ts +6 -0
- package/dist/attempt-state-constants.d.ts.map +1 -0
- package/dist/attempt-state-constants.js +6 -0
- package/dist/attempt-state-constants.js.map +1 -0
- package/dist/attempt-state.d.ts +5 -0
- package/dist/attempt-state.d.ts.map +1 -0
- package/dist/attempt-state.js +190 -0
- package/dist/attempt-state.js.map +1 -0
- package/dist/catalog.js +1 -1
- package/dist/catalog.js.map +1 -1
- package/dist/content-text.d.ts.map +1 -1
- package/dist/content-text.js +3 -0
- package/dist/content-text.js.map +1 -1
- package/dist/custom-operators.d.ts +13 -0
- package/dist/custom-operators.d.ts.map +1 -0
- package/dist/custom-operators.js +2 -0
- package/dist/custom-operators.js.map +1 -0
- package/dist/delivery-redaction.d.ts +50 -0
- package/dist/delivery-redaction.d.ts.map +1 -0
- package/dist/delivery-redaction.js +176 -0
- package/dist/delivery-redaction.js.map +1 -0
- package/dist/delivery-security.d.ts +3 -18
- package/dist/delivery-security.d.ts.map +1 -1
- package/dist/delivery-security.js +35 -189
- package/dist/delivery-security.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/parser-custom-interactions.js +1 -1
- package/dist/parser-custom-interactions.js.map +1 -1
- package/dist/parser-declarations.d.ts.map +1 -1
- package/dist/parser-declarations.js +4 -4
- package/dist/parser-declarations.js.map +1 -1
- package/dist/parser-item-metadata.d.ts.map +1 -1
- package/dist/parser-item-metadata.js +8 -7
- package/dist/parser-item-metadata.js.map +1 -1
- package/dist/parser-processing.js +2 -2
- package/dist/parser-processing.js.map +1 -1
- package/dist/parser-values.d.ts +5 -1
- package/dist/parser-values.d.ts.map +1 -1
- package/dist/parser-values.js +33 -0
- package/dist/parser-values.js.map +1 -1
- package/dist/processing-evaluator-boolean.d.ts +8 -0
- package/dist/processing-evaluator-boolean.d.ts.map +1 -0
- package/dist/processing-evaluator-boolean.js +47 -0
- package/dist/processing-evaluator-boolean.js.map +1 -0
- package/dist/processing-evaluator-collection.d.ts +12 -0
- package/dist/processing-evaluator-collection.d.ts.map +1 -0
- package/dist/processing-evaluator-collection.js +35 -0
- package/dist/processing-evaluator-collection.js.map +1 -0
- package/dist/processing-evaluator-comparison.d.ts +8 -0
- package/dist/processing-evaluator-comparison.d.ts.map +1 -0
- package/dist/processing-evaluator-comparison.js +88 -0
- package/dist/processing-evaluator-comparison.js.map +1 -0
- package/dist/processing-evaluator-custom.d.ts +8 -0
- package/dist/processing-evaluator-custom.d.ts.map +1 -0
- package/dist/processing-evaluator-custom.js +14 -0
- package/dist/processing-evaluator-custom.js.map +1 -0
- package/dist/processing-evaluator-geometry.d.ts +8 -0
- package/dist/processing-evaluator-geometry.d.ts.map +1 -0
- package/dist/processing-evaluator-geometry.js +21 -0
- package/dist/processing-evaluator-geometry.js.map +1 -0
- package/dist/processing-evaluator-numeric.d.ts +8 -0
- package/dist/processing-evaluator-numeric.d.ts.map +1 -0
- package/dist/processing-evaluator-numeric.js +112 -0
- package/dist/processing-evaluator-numeric.js.map +1 -0
- package/dist/processing-evaluator-string.d.ts +8 -0
- package/dist/processing-evaluator-string.d.ts.map +1 -0
- package/dist/processing-evaluator-string.js +33 -0
- package/dist/processing-evaluator-string.js.map +1 -0
- package/dist/processing-evaluator-variable.d.ts +8 -0
- package/dist/processing-evaluator-variable.d.ts.map +1 -0
- package/dist/processing-evaluator-variable.js +39 -0
- package/dist/processing-evaluator-variable.js.map +1 -0
- package/dist/processing-evaluator.d.ts +22 -0
- package/dist/processing-evaluator.d.ts.map +1 -0
- package/dist/processing-evaluator.js +137 -0
- package/dist/processing-evaluator.js.map +1 -0
- package/dist/processing-expression-children.d.ts +3 -0
- package/dist/processing-expression-children.d.ts.map +1 -0
- package/dist/processing-expression-children.js +68 -0
- package/dist/processing-expression-children.js.map +1 -0
- package/dist/processing-mapping.d.ts +13 -0
- package/dist/processing-mapping.d.ts.map +1 -0
- package/dist/processing-mapping.js +120 -0
- package/dist/processing-mapping.js.map +1 -0
- package/dist/processing-operators.d.ts +11 -0
- package/dist/processing-operators.d.ts.map +1 -0
- package/dist/processing-operators.js +178 -0
- package/dist/processing-operators.js.map +1 -0
- package/dist/processing-random.d.ts +2 -0
- package/dist/processing-random.d.ts.map +1 -0
- package/dist/processing-random.js +16 -0
- package/dist/processing-random.js.map +1 -0
- package/dist/processing-state.d.ts +8 -0
- package/dist/processing-state.d.ts.map +1 -0
- package/dist/processing-state.js +44 -0
- package/dist/processing-state.js.map +1 -0
- package/dist/processing-templates.d.ts +3 -0
- package/dist/processing-templates.d.ts.map +1 -0
- package/dist/processing-templates.js +12 -0
- package/dist/processing-templates.js.map +1 -0
- package/dist/processing-values.d.ts +15 -0
- package/dist/processing-values.d.ts.map +1 -0
- package/dist/processing-values.js +150 -0
- package/dist/processing-values.js.map +1 -0
- package/dist/processing-variables.d.ts +7 -0
- package/dist/processing-variables.d.ts.map +1 -0
- package/dist/processing-variables.js +40 -0
- package/dist/processing-variables.js.map +1 -0
- package/dist/serializer-processing-expressions.d.ts.map +1 -1
- package/dist/serializer-processing-expressions.js +2 -8
- package/dist/serializer-processing-expressions.js.map +1 -1
- package/dist/serializer-processing-xml.js +2 -2
- package/dist/serializer-processing-xml.js.map +1 -1
- package/dist/serializer-processing.d.ts.map +1 -1
- package/dist/serializer-processing.js +3 -0
- package/dist/serializer-processing.js.map +1 -1
- package/dist/server-scoring.d.ts +4 -14
- package/dist/server-scoring.d.ts.map +1 -1
- package/dist/server-scoring.js +19 -157
- package/dist/server-scoring.js.map +1 -1
- package/dist/session.d.ts +5 -14
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +76 -1158
- package/dist/session.js.map +1 -1
- package/dist/shared-vocabulary-coverage-policy.js +1 -1
- package/dist/shared-vocabulary-coverage-policy.js.map +1 -1
- package/dist/shared-vocabulary-generated-families.js +1 -1
- package/dist/shared-vocabulary-generated-families.js.map +1 -1
- package/dist/shared-vocabulary-support.js +3 -3
- package/dist/shared-vocabulary-support.js.map +1 -1
- package/dist/shared-vocabulary.d.ts.map +1 -1
- package/dist/shared-vocabulary.js +4 -3
- package/dist/shared-vocabulary.js.map +1 -1
- package/dist/support-evidence.d.ts +22 -0
- package/dist/support-evidence.d.ts.map +1 -0
- package/dist/support-evidence.js +93 -0
- package/dist/support-evidence.js.map +1 -0
- package/dist/support.d.ts.map +1 -1
- package/dist/support.js +38 -47
- package/dist/support.js.map +1 -1
- package/dist/trusted-item-session.d.ts +49 -0
- package/dist/trusted-item-session.d.ts.map +1 -0
- package/dist/trusted-item-session.js +261 -0
- package/dist/trusted-item-session.js.map +1 -0
- package/dist/tts.d.ts.map +1 -1
- package/dist/tts.js +16 -2
- package/dist/tts.js.map +1 -1
- package/dist/validation-geometry.d.ts +8 -0
- package/dist/validation-geometry.d.ts.map +1 -0
- package/dist/validation-geometry.js +187 -0
- package/dist/validation-geometry.js.map +1 -0
- package/dist/validation-interactions.d.ts +3 -0
- package/dist/validation-interactions.d.ts.map +1 -0
- package/dist/validation-interactions.js +644 -0
- package/dist/validation-interactions.js.map +1 -0
- package/dist/validation-primitives.d.ts +11 -0
- package/dist/validation-primitives.d.ts.map +1 -0
- package/dist/validation-primitives.js +48 -0
- package/dist/validation-primitives.js.map +1 -0
- package/dist/validation-processing.d.ts +4 -0
- package/dist/validation-processing.d.ts.map +1 -0
- package/dist/validation-processing.js +569 -0
- package/dist/validation-processing.js.map +1 -0
- package/dist/validation-shared-vocabulary-layout.d.ts +10 -0
- package/dist/validation-shared-vocabulary-layout.d.ts.map +1 -0
- package/dist/validation-shared-vocabulary-layout.js +102 -0
- package/dist/validation-shared-vocabulary-layout.js.map +1 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +27 -1641
- package/dist/validation.js.map +1 -1
- package/dist/xml.js +1 -1
- package/dist/xml.js.map +1 -1
- package/package.json +4 -2
- package/src/adaptive-turn-materializer.ts +85 -0
- package/src/adaptive-turn.ts +109 -0
- package/src/assert-never.ts +4 -0
- package/src/attempt-state-constants.ts +5 -0
- package/src/attempt-state.ts +245 -0
- package/src/catalog.ts +1 -1
- package/src/content-text.ts +3 -0
- package/src/custom-operators.ts +12 -0
- package/src/delivery-redaction.ts +268 -0
- package/src/delivery-security.ts +59 -245
- package/src/index.ts +9 -0
- package/src/parser-custom-interactions.ts +1 -1
- package/src/parser-declarations.ts +4 -3
- package/src/parser-item-metadata.ts +8 -7
- package/src/parser-processing.ts +2 -2
- package/src/parser-values.ts +36 -0
- package/src/processing-evaluator-boolean.ts +48 -0
- package/src/processing-evaluator-collection.ts +50 -0
- package/src/processing-evaluator-comparison.ts +106 -0
- package/src/processing-evaluator-custom.ts +20 -0
- package/src/processing-evaluator-geometry.ts +26 -0
- package/src/processing-evaluator-numeric.ts +155 -0
- package/src/processing-evaluator-string.ts +60 -0
- package/src/processing-evaluator-variable.ts +81 -0
- package/src/processing-evaluator.ts +173 -0
- package/src/processing-expression-children.ts +69 -0
- package/src/processing-mapping.ts +153 -0
- package/src/processing-operators.ts +184 -0
- package/src/processing-random.ts +16 -0
- package/src/processing-state.ts +69 -0
- package/src/processing-templates.ts +12 -0
- package/src/processing-values.ts +162 -0
- package/src/processing-variables.ts +83 -0
- package/src/serializer-processing-expressions.ts +2 -12
- package/src/serializer-processing-xml.ts +2 -2
- package/src/serializer-processing.ts +3 -0
- package/src/server-scoring.ts +30 -226
- package/src/session.ts +128 -2006
- package/src/shared-vocabulary-coverage-policy.ts +1 -1
- package/src/shared-vocabulary-generated-families.ts +1 -1
- package/src/shared-vocabulary-support.ts +3 -3
- package/src/shared-vocabulary.ts +3 -2
- package/src/support-evidence.ts +107 -0
- package/src/support.ts +68 -54
- package/src/trusted-item-session.ts +429 -0
- package/src/tts.ts +29 -15
- package/src/validation-geometry.ts +204 -0
- package/src/validation-interactions.ts +831 -0
- package/src/validation-primitives.ts +63 -0
- package/src/validation-processing.ts +785 -0
- package/src/validation-shared-vocabulary-layout.ts +117 -0
- package/src/validation.ts +44 -2077
- package/src/xml.ts +1 -1
- package/src/interaction-test-fixtures.ts +0 -44
- package/src/serializer-processing.fixtures.ts +0 -469
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import { parseQtiXml } from "./parser.js";
|
|
2
|
+
import { createItemSession, isQtiAttemptStateV1, type QtiItemSession } from "./session.js";
|
|
3
|
+
import type {
|
|
4
|
+
QtiAssessmentItem,
|
|
5
|
+
QtiAttemptStateV1,
|
|
6
|
+
QtiAttemptStatus,
|
|
7
|
+
QtiDiagnostic,
|
|
8
|
+
QtiDocument,
|
|
9
|
+
QtiPortableCustomStateValue,
|
|
10
|
+
QtiValue,
|
|
11
|
+
} from "./types.js";
|
|
12
|
+
import { isQtiPortableCustomStateValue, readQtiJsonValue } from "./value-format.js";
|
|
13
|
+
|
|
14
|
+
export type QtiTrustedInputDiagnosticPrefix = "serverScoring" | "adaptiveTurn";
|
|
15
|
+
|
|
16
|
+
export interface QtiTrustedResponseInput {
|
|
17
|
+
identifier: string;
|
|
18
|
+
value: unknown;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type QtiTrustedResponsesInput =
|
|
22
|
+
| Record<string, unknown>
|
|
23
|
+
| readonly QtiTrustedResponseInput[]
|
|
24
|
+
| undefined;
|
|
25
|
+
|
|
26
|
+
/** Trusted candidate responses and portable custom interaction state for one application. */
|
|
27
|
+
export interface QtiTrustedResponseApplication {
|
|
28
|
+
trustedResponses?: QtiTrustedResponsesInput;
|
|
29
|
+
trustedInteractionStates?: Record<string, QtiPortableCustomStateValue> | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface QtiTrustedItemParseOptions {
|
|
33
|
+
allowedUndeclaredResponseIdentifiers?: readonly string[] | undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface QtiParsedTrustedItem {
|
|
37
|
+
document: QtiDocument;
|
|
38
|
+
diagnostics: QtiDiagnostic[];
|
|
39
|
+
responseIdentifiers: Set<string>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type QtiTrustedItemScoringPolicy = "always" | "onSubmission";
|
|
43
|
+
|
|
44
|
+
export interface RunTrustedItemSessionInput extends QtiTrustedItemParseOptions {
|
|
45
|
+
itemXml: string;
|
|
46
|
+
diagnosticPrefix: QtiTrustedInputDiagnosticPrefix;
|
|
47
|
+
submission: QtiTrustedResponseApplication;
|
|
48
|
+
priorState?: QtiAttemptStateV1 | null | undefined;
|
|
49
|
+
/** Applied before submission. Intended for server-side scoring, not adaptive turns. */
|
|
50
|
+
attemptStatus?: QtiAttemptStatus | undefined;
|
|
51
|
+
scoring: QtiTrustedItemScoringPolicy;
|
|
52
|
+
requireNumericScore: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface RunTrustedItemSessionSuccess {
|
|
56
|
+
ok: true;
|
|
57
|
+
diagnostics: QtiDiagnostic[];
|
|
58
|
+
state: QtiAttemptStateV1;
|
|
59
|
+
responses: Record<string, QtiValue>;
|
|
60
|
+
outcomes: Record<string, QtiValue>;
|
|
61
|
+
score: number | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface RunTrustedItemSessionFailure {
|
|
65
|
+
ok: false;
|
|
66
|
+
diagnostics: QtiDiagnostic[];
|
|
67
|
+
state: QtiAttemptStateV1 | null;
|
|
68
|
+
responses: Record<string, QtiValue>;
|
|
69
|
+
outcomes: Record<string, QtiValue>;
|
|
70
|
+
score: number | null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type RunTrustedItemSessionResult =
|
|
74
|
+
| RunTrustedItemSessionSuccess
|
|
75
|
+
| RunTrustedItemSessionFailure;
|
|
76
|
+
|
|
77
|
+
export function readPriorAttemptState(
|
|
78
|
+
priorState: unknown,
|
|
79
|
+
diagnosticPrefix: QtiTrustedInputDiagnosticPrefix,
|
|
80
|
+
): { state: QtiAttemptStateV1 | null; diagnostics: QtiDiagnostic[] } {
|
|
81
|
+
if (priorState === undefined || priorState === null) return { state: null, diagnostics: [] };
|
|
82
|
+
if (isQtiAttemptStateV1(priorState)) return { state: priorState, diagnostics: [] };
|
|
83
|
+
return {
|
|
84
|
+
state: null,
|
|
85
|
+
diagnostics: [
|
|
86
|
+
{
|
|
87
|
+
code: `${diagnosticPrefix}.state.value`,
|
|
88
|
+
severity: "error",
|
|
89
|
+
message: "Prior adaptive turn state is not a valid qti3.attempt-state.v1 value.",
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function runTrustedItemSession(
|
|
96
|
+
input: RunTrustedItemSessionInput,
|
|
97
|
+
): RunTrustedItemSessionResult {
|
|
98
|
+
const parsedResult = parseTrustedItemXml(
|
|
99
|
+
input.itemXml,
|
|
100
|
+
input.allowedUndeclaredResponseIdentifiers,
|
|
101
|
+
);
|
|
102
|
+
if (!parsedResult.ok) {
|
|
103
|
+
return emptyTrustedItemSessionFailure(parsedResult.diagnostics);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const sessionResult = createTrustedItemSession(
|
|
107
|
+
parsedResult.parsed,
|
|
108
|
+
input.priorState,
|
|
109
|
+
input.diagnosticPrefix,
|
|
110
|
+
input.allowedUndeclaredResponseIdentifiers,
|
|
111
|
+
parsedResult.parsed.diagnostics,
|
|
112
|
+
);
|
|
113
|
+
if (!sessionResult.ok) {
|
|
114
|
+
return emptyTrustedItemSessionFailure(sessionResult.diagnostics);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (input.attemptStatus) sessionResult.session.setStatus(input.attemptStatus);
|
|
118
|
+
|
|
119
|
+
const applicationResult = applyTrustedResponseApplication(
|
|
120
|
+
sessionResult.session,
|
|
121
|
+
parsedResult.parsed,
|
|
122
|
+
input.submission,
|
|
123
|
+
input.allowedUndeclaredResponseIdentifiers,
|
|
124
|
+
input.diagnosticPrefix,
|
|
125
|
+
);
|
|
126
|
+
const diagnostics = applicationResult.diagnostics;
|
|
127
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
128
|
+
return emptyTrustedItemSessionFailure(diagnostics);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const shouldScore = input.scoring === "always" || applicationResult.appliedSubmission;
|
|
132
|
+
|
|
133
|
+
let outcomes = sessionResult.session.serialize().outcomes;
|
|
134
|
+
let state = sessionResult.session.serialize();
|
|
135
|
+
let score: number | null = null;
|
|
136
|
+
let scoredDiagnostics = diagnostics;
|
|
137
|
+
|
|
138
|
+
if (shouldScore) {
|
|
139
|
+
const scoredResult = scoreTrustedItemSession(
|
|
140
|
+
sessionResult.session,
|
|
141
|
+
diagnostics,
|
|
142
|
+
input.diagnosticPrefix,
|
|
143
|
+
);
|
|
144
|
+
if (!scoredResult.ok) {
|
|
145
|
+
return emptyTrustedItemSessionFailure(scoredResult.diagnostics);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
scoredDiagnostics = [...diagnostics, ...scoredResult.scored.diagnostics];
|
|
149
|
+
outcomes = scoredResult.scored.outcomes;
|
|
150
|
+
state = stripUndeclaredResponses(
|
|
151
|
+
scoredResult.scored.state,
|
|
152
|
+
parsedResult.parsed.responseIdentifiers,
|
|
153
|
+
);
|
|
154
|
+
score = readNumericScore(outcomes.SCORE);
|
|
155
|
+
|
|
156
|
+
if (scoredResult.scored.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
157
|
+
return emptyTrustedItemSessionFailure(scoredDiagnostics, {
|
|
158
|
+
state,
|
|
159
|
+
responses: state.responses,
|
|
160
|
+
outcomes,
|
|
161
|
+
score,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (
|
|
166
|
+
input.requireNumericScore &&
|
|
167
|
+
shouldRequireNumericScore(parsedResult.parsed.document.item) &&
|
|
168
|
+
score === null
|
|
169
|
+
) {
|
|
170
|
+
return emptyTrustedItemSessionFailure(
|
|
171
|
+
[...scoredDiagnostics, missingNumericScoreDiagnostic(input.diagnosticPrefix)],
|
|
172
|
+
{
|
|
173
|
+
state,
|
|
174
|
+
responses: state.responses,
|
|
175
|
+
outcomes,
|
|
176
|
+
score,
|
|
177
|
+
},
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
state = stripUndeclaredResponses(state, parsedResult.parsed.responseIdentifiers);
|
|
182
|
+
outcomes = state.outcomes;
|
|
183
|
+
score = readNumericScore(outcomes.SCORE);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
ok: true,
|
|
188
|
+
diagnostics: scoredDiagnostics,
|
|
189
|
+
state,
|
|
190
|
+
responses: state.responses,
|
|
191
|
+
outcomes,
|
|
192
|
+
score,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function emptyTrustedItemSessionFailure(
|
|
197
|
+
diagnostics: QtiDiagnostic[],
|
|
198
|
+
partial: Partial<
|
|
199
|
+
Pick<RunTrustedItemSessionFailure, "state" | "responses" | "outcomes" | "score">
|
|
200
|
+
> = {},
|
|
201
|
+
): RunTrustedItemSessionFailure {
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
diagnostics,
|
|
205
|
+
state: partial.state ?? null,
|
|
206
|
+
responses: partial.responses ?? partial.state?.responses ?? {},
|
|
207
|
+
outcomes: partial.outcomes ?? partial.state?.outcomes ?? {},
|
|
208
|
+
score: partial.score ?? null,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function parseTrustedItemXml(
|
|
213
|
+
itemXml: string,
|
|
214
|
+
allowedUndeclaredResponseIdentifiers: readonly string[] = [],
|
|
215
|
+
): { ok: true; parsed: QtiParsedTrustedItem } | { ok: false; diagnostics: QtiDiagnostic[] } {
|
|
216
|
+
let parsed: ReturnType<typeof parseQtiXml>;
|
|
217
|
+
try {
|
|
218
|
+
parsed = parseQtiXml(itemXml);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
return {
|
|
221
|
+
ok: false,
|
|
222
|
+
diagnostics: [
|
|
223
|
+
{
|
|
224
|
+
code: "xml.parse",
|
|
225
|
+
severity: "error",
|
|
226
|
+
message: error instanceof Error ? error.message : String(error),
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const allowedUndeclared = new Set(allowedUndeclaredResponseIdentifiers);
|
|
233
|
+
const diagnostics = parsed.diagnostics.filter(
|
|
234
|
+
(diagnostic) => !isAllowedUndeclaredVariableReference(diagnostic, allowedUndeclared),
|
|
235
|
+
);
|
|
236
|
+
if (!parsed.document || diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
237
|
+
return { ok: false, diagnostics };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
parsed: {
|
|
243
|
+
document: parsed.document,
|
|
244
|
+
diagnostics,
|
|
245
|
+
responseIdentifiers: new Set(
|
|
246
|
+
parsed.document.item.responseDeclarations.map((declaration) => declaration.identifier),
|
|
247
|
+
),
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function createTrustedItemSession(
|
|
253
|
+
parsed: QtiParsedTrustedItem,
|
|
254
|
+
priorState: QtiAttemptStateV1 | null | undefined,
|
|
255
|
+
diagnosticPrefix: QtiTrustedInputDiagnosticPrefix,
|
|
256
|
+
allowedUndeclaredResponseIdentifiers: readonly string[] | undefined,
|
|
257
|
+
existingDiagnostics: QtiDiagnostic[] = [],
|
|
258
|
+
): { ok: true; session: QtiItemSession } | { ok: false; diagnostics: QtiDiagnostic[] } {
|
|
259
|
+
try {
|
|
260
|
+
return {
|
|
261
|
+
ok: true,
|
|
262
|
+
session: createItemSession(parsed.document, priorState ?? undefined, {
|
|
263
|
+
allowedUndeclaredResponseIdentifiers,
|
|
264
|
+
}),
|
|
265
|
+
};
|
|
266
|
+
} catch (error) {
|
|
267
|
+
return {
|
|
268
|
+
ok: false,
|
|
269
|
+
diagnostics: [
|
|
270
|
+
...existingDiagnostics,
|
|
271
|
+
{
|
|
272
|
+
code: `${diagnosticPrefix}.state.restore`,
|
|
273
|
+
severity: "error",
|
|
274
|
+
message: error instanceof Error ? error.message : String(error),
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function applyTrustedResponseApplication(
|
|
282
|
+
session: QtiItemSession,
|
|
283
|
+
parsed: QtiParsedTrustedItem,
|
|
284
|
+
submission: QtiTrustedResponseApplication,
|
|
285
|
+
allowedUndeclaredResponseIdentifiers: readonly string[] | undefined,
|
|
286
|
+
diagnosticPrefix: QtiTrustedInputDiagnosticPrefix,
|
|
287
|
+
): { diagnostics: QtiDiagnostic[]; appliedSubmission: boolean } {
|
|
288
|
+
const diagnostics = [...parsed.diagnostics];
|
|
289
|
+
const allowedUndeclared = new Set(allowedUndeclaredResponseIdentifiers ?? []);
|
|
290
|
+
let appliedSubmission = false;
|
|
291
|
+
|
|
292
|
+
for (const response of normalizeResponseInputs(submission.trustedResponses)) {
|
|
293
|
+
const identifier = response.identifier.trim();
|
|
294
|
+
if (!identifier) {
|
|
295
|
+
diagnostics.push({
|
|
296
|
+
code: `${diagnosticPrefix}.response.identifier`,
|
|
297
|
+
severity: "error",
|
|
298
|
+
message: "Trusted response identifiers must be non-empty strings.",
|
|
299
|
+
});
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (!parsed.responseIdentifiers.has(identifier) && !allowedUndeclared.has(identifier)) {
|
|
304
|
+
diagnostics.push({
|
|
305
|
+
code: `${diagnosticPrefix}.response.ignored`,
|
|
306
|
+
severity: "warning",
|
|
307
|
+
message: `Trusted response ${identifier} was ignored because it is not declared by the item.`,
|
|
308
|
+
});
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const value = readQtiJsonValue(response.value);
|
|
313
|
+
if (value === undefined) {
|
|
314
|
+
diagnostics.push({
|
|
315
|
+
code: `${diagnosticPrefix}.response.value`,
|
|
316
|
+
severity: "error",
|
|
317
|
+
message: `Trusted response ${identifier} is not a supported QTI value.`,
|
|
318
|
+
});
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
session.respond(identifier, value);
|
|
322
|
+
appliedSubmission = true;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
for (const [identifier, state] of Object.entries(submission.trustedInteractionStates ?? {})) {
|
|
326
|
+
if (!isQtiPortableCustomStateValue(state)) {
|
|
327
|
+
diagnostics.push({
|
|
328
|
+
code: `${diagnosticPrefix}.interactionState.value`,
|
|
329
|
+
severity: "error",
|
|
330
|
+
message: `Trusted interaction state ${identifier} is not a supported portable custom state value.`,
|
|
331
|
+
});
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
session.setInteractionState(identifier, state);
|
|
337
|
+
appliedSubmission = true;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
diagnostics.push({
|
|
340
|
+
code: `${diagnosticPrefix}.interactionState.identifier`,
|
|
341
|
+
severity: "error",
|
|
342
|
+
message: error instanceof Error ? error.message : String(error),
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return { diagnostics, appliedSubmission };
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function scoreTrustedItemSession(
|
|
351
|
+
session: QtiItemSession,
|
|
352
|
+
diagnostics: QtiDiagnostic[],
|
|
353
|
+
diagnosticPrefix: QtiTrustedInputDiagnosticPrefix,
|
|
354
|
+
):
|
|
355
|
+
| { ok: true; scored: ReturnType<QtiItemSession["score"]> }
|
|
356
|
+
| { ok: false; diagnostics: QtiDiagnostic[] } {
|
|
357
|
+
try {
|
|
358
|
+
return { ok: true, scored: session.score() };
|
|
359
|
+
} catch (error) {
|
|
360
|
+
return {
|
|
361
|
+
ok: false,
|
|
362
|
+
diagnostics: [
|
|
363
|
+
...diagnostics,
|
|
364
|
+
{
|
|
365
|
+
code: `${diagnosticPrefix}.score.exception`,
|
|
366
|
+
severity: "error",
|
|
367
|
+
message: error instanceof Error ? error.message : String(error),
|
|
368
|
+
},
|
|
369
|
+
],
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function shouldRequireNumericScore(item: QtiAssessmentItem): boolean {
|
|
375
|
+
return (
|
|
376
|
+
Boolean(item.responseProcessing) ||
|
|
377
|
+
item.outcomeDeclarations.some((declaration) => declaration.identifier === "SCORE")
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function readNumericScore(value: QtiValue | undefined): number | null {
|
|
382
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
383
|
+
if (typeof value === "string") {
|
|
384
|
+
const score = Number(value.trim());
|
|
385
|
+
return Number.isFinite(score) ? score : null;
|
|
386
|
+
}
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function missingNumericScoreDiagnostic(
|
|
391
|
+
diagnosticPrefix: QtiTrustedInputDiagnosticPrefix,
|
|
392
|
+
): QtiDiagnostic {
|
|
393
|
+
return {
|
|
394
|
+
code: `${diagnosticPrefix}.score.missing`,
|
|
395
|
+
severity: "error",
|
|
396
|
+
message: "Server-side scoring did not produce a numeric SCORE outcome.",
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function stripUndeclaredResponses(
|
|
401
|
+
state: QtiAttemptStateV1,
|
|
402
|
+
declaredResponseIdentifiers: Set<string>,
|
|
403
|
+
): QtiAttemptStateV1 {
|
|
404
|
+
return {
|
|
405
|
+
...state,
|
|
406
|
+
responses: Object.fromEntries(
|
|
407
|
+
Object.entries(state.responses).filter(([identifier]) =>
|
|
408
|
+
declaredResponseIdentifiers.has(identifier),
|
|
409
|
+
),
|
|
410
|
+
),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function normalizeResponseInputs(responses: QtiTrustedResponsesInput): QtiTrustedResponseInput[] {
|
|
415
|
+
if (!responses) return [];
|
|
416
|
+
if (Array.isArray(responses)) return [...responses];
|
|
417
|
+
return Object.entries(responses).map(([identifier, value]) => ({ identifier, value }));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function isAllowedUndeclaredVariableReference(
|
|
421
|
+
diagnostic: QtiDiagnostic,
|
|
422
|
+
allowedUndeclaredResponseIdentifiers: Set<string>,
|
|
423
|
+
): boolean {
|
|
424
|
+
if (diagnostic.code !== "processing.variable.reference") return false;
|
|
425
|
+
const identifier = diagnostic.message.match(
|
|
426
|
+
/^Processing expression references missing variable (.+)\.$/,
|
|
427
|
+
)?.[1];
|
|
428
|
+
return Boolean(identifier && allowedUndeclaredResponseIdentifiers.has(identifier));
|
|
429
|
+
}
|
package/src/tts.ts
CHANGED
|
@@ -103,10 +103,17 @@ interface SegmentInput {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const dataSsmlFunctionNames = new Set(["break", "phoneme", "prosody", "say-as", "sub"]);
|
|
106
|
-
const breakStrengths = new Set([
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
const breakStrengths = new Set<QtiDataSsmlBreakStrength>([
|
|
107
|
+
"medium",
|
|
108
|
+
"none",
|
|
109
|
+
"strong",
|
|
110
|
+
"weak",
|
|
111
|
+
"x-strong",
|
|
112
|
+
"x-weak",
|
|
113
|
+
]);
|
|
114
|
+
const phonemeAlphabets = new Set<NonNullable<QtiDataSsmlPhoneme["alphabet"]>>(["ipa", "x-sampa"]);
|
|
115
|
+
const phonemeTypes = new Set<NonNullable<QtiDataSsmlPhoneme["type"]>>(["default", "ruby"]);
|
|
116
|
+
const sayAsInterpretations = new Set<QtiDataSsmlSayAs["interpret-as"]>([
|
|
110
117
|
"cardinal",
|
|
111
118
|
"characters",
|
|
112
119
|
"date",
|
|
@@ -200,7 +207,7 @@ function validateBreak(value: Record<string, unknown>, errors: string[]): QtiDat
|
|
|
200
207
|
const result: QtiDataSsmlBreak = {};
|
|
201
208
|
const strength = optionalEnum(value, "strength", "break.strength", breakStrengths, errors);
|
|
202
209
|
const time = optionalString(value, "time", "break.time", errors);
|
|
203
|
-
if (strength) result.strength = strength
|
|
210
|
+
if (strength) result.strength = strength;
|
|
204
211
|
if (time !== undefined) result.time = time;
|
|
205
212
|
return result;
|
|
206
213
|
}
|
|
@@ -215,8 +222,8 @@ function validatePhoneme(
|
|
|
215
222
|
const type = optionalEnum(value, "type", "phoneme.type", phonemeTypes, errors);
|
|
216
223
|
if (!ph) return undefined;
|
|
217
224
|
const result: QtiDataSsmlPhoneme = { ph };
|
|
218
|
-
if (alphabet) result.alphabet = alphabet
|
|
219
|
-
if (type) result.type = type
|
|
225
|
+
if (alphabet) result.alphabet = alphabet;
|
|
226
|
+
if (type) result.type = type;
|
|
220
227
|
return result;
|
|
221
228
|
}
|
|
222
229
|
|
|
@@ -241,7 +248,7 @@ function validateSayAs(
|
|
|
241
248
|
errors,
|
|
242
249
|
);
|
|
243
250
|
if (!interpretation) return undefined;
|
|
244
|
-
return { "interpret-as": interpretation
|
|
251
|
+
return { "interpret-as": interpretation };
|
|
245
252
|
}
|
|
246
253
|
|
|
247
254
|
function validateSub(value: Record<string, unknown>, errors: string[]): QtiDataSsmlSub | undefined {
|
|
@@ -520,29 +527,36 @@ function requiredString(
|
|
|
520
527
|
return optionalString(value, name, path, errors);
|
|
521
528
|
}
|
|
522
529
|
|
|
523
|
-
function
|
|
530
|
+
function isAllowedEnum<T extends string>(value: string, allowed: ReadonlySet<T>): value is T {
|
|
531
|
+
for (const entry of allowed) {
|
|
532
|
+
if (entry === value) return true;
|
|
533
|
+
}
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function optionalEnum<T extends string>(
|
|
524
538
|
value: Record<string, unknown>,
|
|
525
539
|
name: string,
|
|
526
540
|
path: string,
|
|
527
|
-
allowed:
|
|
541
|
+
allowed: ReadonlySet<T>,
|
|
528
542
|
errors: string[],
|
|
529
|
-
):
|
|
543
|
+
): T | undefined {
|
|
530
544
|
const property = optionalString(value, name, path, errors);
|
|
531
545
|
if (property === undefined) return undefined;
|
|
532
|
-
if (!
|
|
546
|
+
if (!isAllowedEnum(property, allowed)) {
|
|
533
547
|
errors.push(`${path} has unsupported value "${property}".`);
|
|
534
548
|
return undefined;
|
|
535
549
|
}
|
|
536
550
|
return property;
|
|
537
551
|
}
|
|
538
552
|
|
|
539
|
-
function requiredEnum(
|
|
553
|
+
function requiredEnum<T extends string>(
|
|
540
554
|
value: Record<string, unknown>,
|
|
541
555
|
name: string,
|
|
542
556
|
path: string,
|
|
543
|
-
allowed:
|
|
557
|
+
allowed: ReadonlySet<T>,
|
|
544
558
|
errors: string[],
|
|
545
|
-
):
|
|
559
|
+
): T | undefined {
|
|
546
560
|
if (!Object.hasOwn(value, name)) {
|
|
547
561
|
errors.push(`${path} is required.`);
|
|
548
562
|
return undefined;
|