@longsightgroup/qti3-core 0.9.0 → 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.
Files changed (51) hide show
  1. package/README.md +39 -0
  2. package/dist/adaptive-turn-materializer.d.ts +14 -0
  3. package/dist/adaptive-turn-materializer.d.ts.map +1 -0
  4. package/dist/adaptive-turn-materializer.js +56 -0
  5. package/dist/adaptive-turn-materializer.js.map +1 -0
  6. package/dist/adaptive-turn.d.ts +23 -0
  7. package/dist/adaptive-turn.d.ts.map +1 -0
  8. package/dist/adaptive-turn.js +60 -0
  9. package/dist/adaptive-turn.js.map +1 -0
  10. package/dist/asset-url.d.ts.map +1 -1
  11. package/dist/asset-url.js +1 -0
  12. package/dist/asset-url.js.map +1 -1
  13. package/dist/delivery-redaction.d.ts +50 -0
  14. package/dist/delivery-redaction.d.ts.map +1 -0
  15. package/dist/delivery-redaction.js +176 -0
  16. package/dist/delivery-redaction.js.map +1 -0
  17. package/dist/delivery-security.d.ts +3 -18
  18. package/dist/delivery-security.d.ts.map +1 -1
  19. package/dist/delivery-security.js +35 -189
  20. package/dist/delivery-security.js.map +1 -1
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -0
  24. package/dist/index.js.map +1 -1
  25. package/dist/server-scoring.d.ts +4 -14
  26. package/dist/server-scoring.d.ts.map +1 -1
  27. package/dist/server-scoring.js +19 -157
  28. package/dist/server-scoring.js.map +1 -1
  29. package/dist/shared-vocabulary-support.d.ts.map +1 -1
  30. package/dist/shared-vocabulary-support.js +1 -0
  31. package/dist/shared-vocabulary-support.js.map +1 -1
  32. package/dist/support.d.ts.map +1 -1
  33. package/dist/support.js +8 -4
  34. package/dist/support.js.map +1 -1
  35. package/dist/trusted-item-session.d.ts +49 -0
  36. package/dist/trusted-item-session.d.ts.map +1 -0
  37. package/dist/trusted-item-session.js +260 -0
  38. package/dist/trusted-item-session.js.map +1 -0
  39. package/package.json +4 -2
  40. package/src/adaptive-turn-materializer.ts +85 -0
  41. package/src/adaptive-turn.ts +109 -0
  42. package/src/asset-url.ts +1 -0
  43. package/src/delivery-redaction.ts +268 -0
  44. package/src/delivery-security.ts +59 -245
  45. package/src/index.ts +7 -0
  46. package/src/server-scoring.ts +30 -226
  47. package/src/shared-vocabulary-support.ts +1 -0
  48. package/src/support.ts +9 -4
  49. package/src/trusted-item-session.ts +427 -0
  50. package/src/interaction-test-fixtures.ts +0 -44
  51. package/src/serializer-processing.fixtures.ts +0 -469
@@ -1,32 +1,17 @@
1
- import { createItemSession } from "./session.js";
2
- import { parseQtiXml } from "./parser.js";
3
- import type {
4
- QtiAssessmentItem,
5
- QtiAttemptStateV1,
6
- QtiAttemptStatus,
7
- QtiDiagnostic,
8
- QtiPortableCustomStateValue,
9
- QtiScoreResult,
10
- QtiValue,
11
- } from "./types.js";
12
- import { isQtiPortableCustomStateValue, readQtiJsonValue } from "./value-format.js";
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
- let parsed: ReturnType<typeof parseQtiXml>;
43
- try {
44
- parsed = parseQtiXml(input.itemXml);
45
- } catch (error) {
46
- return failed([
47
- {
48
- code: "xml.parse",
49
- severity: "error",
50
- message: error instanceof Error ? error.message : String(error),
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
- ...state,
238
- responses: Object.fromEntries(
239
- Object.entries(state.responses).filter(([identifier]) =>
240
- declaredResponseIdentifiers.has(identifier),
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
  }
@@ -45,6 +45,7 @@ const sharedVocabularyValidationTests = [
45
45
  const choiceLayoutTests = ["packages/player/src/interactions/choice-layout.test.ts"];
46
46
  const browserBehaviorTests = [
47
47
  "tests/browser/player-dom-behavior.spec.ts",
48
+ "tests/browser/player-gap-match.spec.ts",
48
49
  ...sharedVocabularyMatrixTests,
49
50
  ];
50
51
  const graphicBrowserTests = [
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: "parsed",
215
+ support: "rendered",
216
216
  specReference: "QTI 3.0.1 ASI",
217
217
  parse: true,
218
218
  validate: true,
219
- render: false,
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: ["packages/core/src/core.test.ts", "packages/core/src/parser-item-metadata.test.ts"],
225
- notes: "Parsed by parser-item-metadata.ts and validated by validateStylesheets.",
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",