@longsightgroup/qti3-core 0.9.1 → 0.9.2
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 +39 -0
- 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/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 +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.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/support.d.ts.map +1 -1
- package/dist/support.js +8 -4
- 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 +260 -0
- package/dist/trusted-item-session.js.map +1 -0
- package/package.json +4 -2
- package/src/adaptive-turn-materializer.ts +85 -0
- package/src/adaptive-turn.ts +109 -0
- package/src/delivery-redaction.ts +268 -0
- package/src/delivery-security.ts +59 -245
- package/src/index.ts +7 -0
- package/src/server-scoring.ts +30 -226
- package/src/support.ts +9 -4
- package/src/trusted-item-session.ts +427 -0
- package/src/interaction-test-fixtures.ts +0 -44
- package/src/serializer-processing.fixtures.ts +0 -469
package/src/server-scoring.ts
CHANGED
|
@@ -1,32 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
export interface QtiServerScoringResponseInput {
|
|
15
|
-
identifier: string;
|
|
16
|
-
value: unknown;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface QtiServerScoringInput {
|
|
1
|
+
import type { QtiAttemptStateV1, QtiAttemptStatus, QtiDiagnostic, QtiValue } from "./types.js";
|
|
2
|
+
import {
|
|
3
|
+
runTrustedItemSession,
|
|
4
|
+
type QtiTrustedItemParseOptions,
|
|
5
|
+
type QtiTrustedResponseApplication,
|
|
6
|
+
type QtiTrustedResponseInput,
|
|
7
|
+
} from "./trusted-item-session.js";
|
|
8
|
+
|
|
9
|
+
export type QtiServerScoringResponseInput = QtiTrustedResponseInput;
|
|
10
|
+
|
|
11
|
+
export interface QtiServerScoringInput
|
|
12
|
+
extends QtiTrustedResponseApplication, QtiTrustedItemParseOptions {
|
|
20
13
|
itemXml: string;
|
|
21
|
-
/**
|
|
22
|
-
* Server-trusted response values. This API validates JSON/QTI value shape and declared
|
|
23
|
-
* identifiers, but it does not run candidate response-validation policy such as required
|
|
24
|
-
* interactions or min/max response counts.
|
|
25
|
-
*/
|
|
26
|
-
trustedResponses?: Record<string, unknown> | readonly QtiServerScoringResponseInput[] | undefined;
|
|
27
|
-
trustedInteractionStates?: Record<string, QtiPortableCustomStateValue> | undefined;
|
|
28
14
|
status?: QtiAttemptStatus | undefined;
|
|
29
|
-
allowedUndeclaredResponseIdentifiers?: readonly string[] | undefined;
|
|
30
15
|
}
|
|
31
16
|
|
|
32
17
|
export interface QtiServerScoringResult {
|
|
@@ -39,206 +24,25 @@ export interface QtiServerScoringResult {
|
|
|
39
24
|
}
|
|
40
25
|
|
|
41
26
|
export function scoreQtiItemServerSide(input: QtiServerScoringInput): QtiServerScoringResult {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const allowedUndeclaredResponseIdentifiers = new Set(
|
|
56
|
-
input.allowedUndeclaredResponseIdentifiers ?? [],
|
|
57
|
-
);
|
|
58
|
-
const parseDiagnostics = parsed.diagnostics.filter(
|
|
59
|
-
(diagnostic) =>
|
|
60
|
-
!isAllowedUndeclaredVariableReference(diagnostic, allowedUndeclaredResponseIdentifiers),
|
|
61
|
-
);
|
|
62
|
-
if (!parsed.document || parseDiagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
63
|
-
return failed(parseDiagnostics);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const diagnostics = [...parseDiagnostics];
|
|
67
|
-
const session = createItemSession(parsed.document);
|
|
68
|
-
if (input.status) session.setStatus(input.status);
|
|
69
|
-
|
|
70
|
-
const responseIdentifiers = new Set(
|
|
71
|
-
parsed.document.item.responseDeclarations.map((declaration) => declaration.identifier),
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
for (const response of normalizeResponseInputs(input.trustedResponses)) {
|
|
75
|
-
const identifier = response.identifier.trim();
|
|
76
|
-
if (!identifier) {
|
|
77
|
-
diagnostics.push({
|
|
78
|
-
code: "serverScoring.response.identifier",
|
|
79
|
-
severity: "error",
|
|
80
|
-
message: "Trusted response identifiers must be non-empty strings.",
|
|
81
|
-
});
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (
|
|
86
|
-
!responseIdentifiers.has(identifier) &&
|
|
87
|
-
!allowedUndeclaredResponseIdentifiers.has(identifier)
|
|
88
|
-
) {
|
|
89
|
-
diagnostics.push({
|
|
90
|
-
code: "serverScoring.response.ignored",
|
|
91
|
-
severity: "warning",
|
|
92
|
-
message: `Trusted response ${identifier} was ignored because it is not declared by the item.`,
|
|
93
|
-
});
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const value = readQtiJsonValue(response.value);
|
|
98
|
-
if (value === undefined) {
|
|
99
|
-
diagnostics.push({
|
|
100
|
-
code: "serverScoring.response.value",
|
|
101
|
-
severity: "error",
|
|
102
|
-
message: `Trusted response ${identifier} is not a supported QTI value.`,
|
|
103
|
-
});
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
session.respond(identifier, value);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
for (const [identifier, state] of Object.entries(input.trustedInteractionStates ?? {})) {
|
|
110
|
-
if (!isQtiPortableCustomStateValue(state)) {
|
|
111
|
-
diagnostics.push({
|
|
112
|
-
code: "serverScoring.interactionState.value",
|
|
113
|
-
severity: "error",
|
|
114
|
-
message: `Trusted interaction state ${identifier} is not a supported portable custom state value.`,
|
|
115
|
-
});
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
try {
|
|
120
|
-
session.setInteractionState(identifier, state);
|
|
121
|
-
} catch (error) {
|
|
122
|
-
diagnostics.push({
|
|
123
|
-
code: "serverScoring.interactionState.identifier",
|
|
124
|
-
severity: "error",
|
|
125
|
-
message: error instanceof Error ? error.message : String(error),
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
131
|
-
return failed(diagnostics);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
let scored: QtiScoreResult;
|
|
135
|
-
try {
|
|
136
|
-
scored = session.score();
|
|
137
|
-
} catch (error) {
|
|
138
|
-
return failed([
|
|
139
|
-
...diagnostics,
|
|
140
|
-
{
|
|
141
|
-
code: "serverScoring.score.exception",
|
|
142
|
-
severity: "error",
|
|
143
|
-
message: error instanceof Error ? error.message : String(error),
|
|
144
|
-
},
|
|
145
|
-
]);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const scoredDiagnostics = [...diagnostics, ...scored.diagnostics];
|
|
149
|
-
const score = readNumericScore(scored.outcomes.SCORE);
|
|
150
|
-
const scoredState = stripUndeclaredResponses(scored.state, responseIdentifiers);
|
|
151
|
-
if (scored.diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
152
|
-
return failed(scoredDiagnostics, scoredState, scored.outcomes, score);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (shouldRequireNumericScore(parsed.document.item) && score === null) {
|
|
156
|
-
return failed(
|
|
157
|
-
[
|
|
158
|
-
...scoredDiagnostics,
|
|
159
|
-
{
|
|
160
|
-
code: "serverScoring.score.missing",
|
|
161
|
-
severity: "error",
|
|
162
|
-
message: "Server-side scoring did not produce a numeric SCORE outcome.",
|
|
163
|
-
},
|
|
164
|
-
],
|
|
165
|
-
scoredState,
|
|
166
|
-
scored.outcomes,
|
|
167
|
-
score,
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
ok: true,
|
|
173
|
-
diagnostics: scoredDiagnostics,
|
|
174
|
-
state: scoredState,
|
|
175
|
-
responses: scoredState.responses,
|
|
176
|
-
outcomes: scored.outcomes,
|
|
177
|
-
score,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function normalizeResponseInputs(
|
|
182
|
-
responses: QtiServerScoringInput["trustedResponses"],
|
|
183
|
-
): QtiServerScoringResponseInput[] {
|
|
184
|
-
if (!responses) return [];
|
|
185
|
-
if (Array.isArray(responses)) return [...responses];
|
|
186
|
-
return Object.entries(responses).map(([identifier, value]) => ({ identifier, value }));
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function shouldRequireNumericScore(item: QtiAssessmentItem): boolean {
|
|
190
|
-
return (
|
|
191
|
-
Boolean(item.responseProcessing) ||
|
|
192
|
-
item.outcomeDeclarations.some((declaration) => declaration.identifier === "SCORE")
|
|
193
|
-
);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function readNumericScore(value: QtiValue | undefined): number | null {
|
|
197
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
198
|
-
if (typeof value === "string") {
|
|
199
|
-
const score = Number(value.trim());
|
|
200
|
-
return Number.isFinite(score) ? score : null;
|
|
201
|
-
}
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function isAllowedUndeclaredVariableReference(
|
|
206
|
-
diagnostic: QtiDiagnostic,
|
|
207
|
-
allowedUndeclaredResponseIdentifiers: Set<string>,
|
|
208
|
-
): boolean {
|
|
209
|
-
if (diagnostic.code !== "processing.variable.reference") return false;
|
|
210
|
-
const identifier = diagnostic.message.match(
|
|
211
|
-
/^Processing expression references missing variable (.+)\.$/,
|
|
212
|
-
)?.[1];
|
|
213
|
-
return Boolean(identifier && allowedUndeclaredResponseIdentifiers.has(identifier));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function failed(
|
|
217
|
-
diagnostics: QtiDiagnostic[],
|
|
218
|
-
state: QtiAttemptStateV1 | null = null,
|
|
219
|
-
outcomes: Record<string, QtiValue> = {},
|
|
220
|
-
score: number | null = null,
|
|
221
|
-
): QtiServerScoringResult {
|
|
222
|
-
return {
|
|
223
|
-
ok: false,
|
|
224
|
-
diagnostics,
|
|
225
|
-
state,
|
|
226
|
-
responses: state?.responses ?? {},
|
|
227
|
-
outcomes,
|
|
228
|
-
score,
|
|
229
|
-
};
|
|
230
|
-
}
|
|
27
|
+
const result = runTrustedItemSession({
|
|
28
|
+
itemXml: input.itemXml,
|
|
29
|
+
diagnosticPrefix: "serverScoring",
|
|
30
|
+
allowedUndeclaredResponseIdentifiers: input.allowedUndeclaredResponseIdentifiers,
|
|
31
|
+
submission: {
|
|
32
|
+
trustedResponses: input.trustedResponses,
|
|
33
|
+
trustedInteractionStates: input.trustedInteractionStates,
|
|
34
|
+
},
|
|
35
|
+
attemptStatus: input.status,
|
|
36
|
+
scoring: "always",
|
|
37
|
+
requireNumericScore: true,
|
|
38
|
+
});
|
|
231
39
|
|
|
232
|
-
function stripUndeclaredResponses(
|
|
233
|
-
state: QtiAttemptStateV1,
|
|
234
|
-
declaredResponseIdentifiers: Set<string>,
|
|
235
|
-
): QtiAttemptStateV1 {
|
|
236
40
|
return {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
41
|
+
ok: result.ok,
|
|
42
|
+
diagnostics: result.diagnostics,
|
|
43
|
+
state: result.state,
|
|
44
|
+
responses: result.responses,
|
|
45
|
+
outcomes: result.outcomes,
|
|
46
|
+
score: result.score,
|
|
243
47
|
};
|
|
244
48
|
}
|
package/src/support.ts
CHANGED
|
@@ -212,17 +212,22 @@ export const itemMetadataSupport: QtiItemMetadataElementSupport[] = [
|
|
|
212
212
|
{
|
|
213
213
|
qtiName: "qti-stylesheet",
|
|
214
214
|
category: "itemMetadata",
|
|
215
|
-
support: "
|
|
215
|
+
support: "rendered",
|
|
216
216
|
specReference: "QTI 3.0.1 ASI",
|
|
217
217
|
parse: true,
|
|
218
218
|
validate: true,
|
|
219
|
-
render:
|
|
219
|
+
render: true,
|
|
220
220
|
process: false,
|
|
221
221
|
fixtures: [
|
|
222
222
|
"packages/fixtures/packages/basic-item-player/valid-item-only/items/tolerance-extra-features.xml",
|
|
223
223
|
],
|
|
224
|
-
tests: [
|
|
225
|
-
|
|
224
|
+
tests: [
|
|
225
|
+
"packages/core/src/core.test.ts",
|
|
226
|
+
"packages/core/src/parser-item-metadata.test.ts",
|
|
227
|
+
"tests/browser/player-package.spec.ts",
|
|
228
|
+
],
|
|
229
|
+
notes:
|
|
230
|
+
"Parsed by parser-item-metadata.ts, validated by validateStylesheets, and delivered by the player only when a host resolveStylesheet hook returns a safe resolved stylesheet URL.",
|
|
226
231
|
},
|
|
227
232
|
{
|
|
228
233
|
qtiName: "qti-modal-feedback",
|