@superbuilders/incept-renderer 0.1.0 → 0.1.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 +632 -88
- package/dist/actions/index.d.ts +1 -1
- package/dist/actions/index.js +362 -552
- package/dist/actions/index.js.map +1 -1
- package/dist/components/index.d.ts +3 -3
- package/dist/components/index.js +71 -115
- package/dist/components/index.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.js +415 -461
- package/dist/index.js.map +1 -1
- package/dist/{schema-DxNEXGoq.d.ts → schema-CkAPLwco.d.ts} +5 -5
- package/dist/{types-MOyn9ktl.d.ts → types-DIDj-78l.d.ts} +1 -1
- package/package.json +16 -3
package/dist/actions/index.js
CHANGED
|
@@ -1,202 +1,14 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
|
-
import '
|
|
3
|
-
import '
|
|
4
|
-
import 'tailwind-merge';
|
|
5
|
-
import 'react-dom';
|
|
6
|
-
import 'react/jsx-runtime';
|
|
7
|
-
import '@radix-ui/react-radio-group';
|
|
8
|
-
import '@radix-ui/react-checkbox';
|
|
9
|
-
import 'class-variance-authority';
|
|
10
|
-
import '@radix-ui/react-label';
|
|
11
|
-
import '@radix-ui/react-separator';
|
|
12
|
-
import 'lucide-react';
|
|
13
|
-
import '@dnd-kit/core';
|
|
14
|
-
import '@radix-ui/react-select';
|
|
15
|
-
import '@dnd-kit/sortable';
|
|
16
|
-
import '@dnd-kit/utilities';
|
|
2
|
+
import * as errors from '@superbuilders/errors';
|
|
3
|
+
import * as logger2 from '@superbuilders/slog';
|
|
17
4
|
import { XMLParser } from 'fast-xml-parser';
|
|
18
|
-
import { z
|
|
5
|
+
import { z } from 'zod';
|
|
19
6
|
|
|
20
7
|
// ../../../../../node_modules/server-only/index.js
|
|
21
8
|
throw new Error(
|
|
22
9
|
"This module cannot be imported from a Client Component module. It should only be used from a Server Component."
|
|
23
10
|
);
|
|
24
11
|
|
|
25
|
-
// ../../node_modules/@superbuilders/errors/dist/index.js
|
|
26
|
-
function z() {
|
|
27
|
-
let k2 = [], j2 = this;
|
|
28
|
-
while (j2 != null) if (k2.push(j2.message), j2.cause instanceof Error) j2 = j2.cause;
|
|
29
|
-
else break;
|
|
30
|
-
return k2.join(": ");
|
|
31
|
-
}
|
|
32
|
-
function A(k2) {
|
|
33
|
-
let j2 = new Error(k2);
|
|
34
|
-
if (Error.captureStackTrace) Error.captureStackTrace(j2, A);
|
|
35
|
-
return j2.toString = z, Object.freeze(j2);
|
|
36
|
-
}
|
|
37
|
-
function B(k2, j2) {
|
|
38
|
-
let x = new Error(j2, { cause: k2 });
|
|
39
|
-
if (Error.captureStackTrace) Error.captureStackTrace(x, B);
|
|
40
|
-
return x.toString = z, Object.freeze(x);
|
|
41
|
-
}
|
|
42
|
-
function I(k2) {
|
|
43
|
-
try {
|
|
44
|
-
return { data: k2(), error: void 0 };
|
|
45
|
-
} catch (j2) {
|
|
46
|
-
return { data: void 0, error: j2 instanceof Error ? j2 : new Error(String(j2)) };
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ../../node_modules/@superbuilders/slog/dist/index.js
|
|
51
|
-
var b = new ArrayBuffer(8192);
|
|
52
|
-
var K = new Uint8Array(b);
|
|
53
|
-
var Q = new TextEncoder();
|
|
54
|
-
var C = new ArrayBuffer(64);
|
|
55
|
-
var L = new Uint8Array(C);
|
|
56
|
-
var J = { DEBUG: Q.encode(" DEBUG "), INFO: Q.encode(" INFO "), WARN: Q.encode(" WARN "), ERROR: Q.encode(" ERROR "), NEWLINE: Q.encode(`
|
|
57
|
-
`), SPACE: Q.encode(" "), EQUALS: Q.encode("="), SLASH: Q.encode("/"), COLON: Q.encode(":"), NULL: Q.encode("null"), UNDEFINED: Q.encode("undefined"), TRUE: Q.encode("true"), FALSE: Q.encode("false"), QUOTE: Q.encode('"'), BRACKET_OPEN: Q.encode("["), BRACKET_CLOSE: Q.encode("]"), BRACE_OPEN: Q.encode("{"), BRACE_CLOSE: Q.encode("}"), COMMA: Q.encode(","), ZERO: Q.encode("0"), MINUS: Q.encode("-"), DOT: Q.encode(".") };
|
|
58
|
-
var U = new Uint8Array(19);
|
|
59
|
-
var z2 = new Uint8Array(2);
|
|
60
|
-
var V = 0;
|
|
61
|
-
var F = 0;
|
|
62
|
-
var W = 0;
|
|
63
|
-
function j() {
|
|
64
|
-
let x = Date.now();
|
|
65
|
-
if (x - V >= 1e3) {
|
|
66
|
-
let q = new Date(x), D = 0, R = q.getFullYear();
|
|
67
|
-
U[D++] = 48 + Math.floor(R / 1e3) % 10, U[D++] = 48 + Math.floor(R / 100) % 10, U[D++] = 48 + Math.floor(R / 10) % 10, U[D++] = 48 + R % 10, U[D++] = 47;
|
|
68
|
-
let N = q.getMonth() + 1;
|
|
69
|
-
U[D++] = 48 + Math.floor(N / 10), U[D++] = 48 + N % 10, U[D++] = 47;
|
|
70
|
-
let P = q.getDate();
|
|
71
|
-
U[D++] = 48 + Math.floor(P / 10), U[D++] = 48 + P % 10, U[D++] = 32;
|
|
72
|
-
let M = q.getHours();
|
|
73
|
-
U[D++] = 48 + Math.floor(M / 10), U[D++] = 48 + M % 10, U[D++] = 58;
|
|
74
|
-
let Z = q.getMinutes();
|
|
75
|
-
U[D++] = 48 + Math.floor(Z / 10), U[D++] = 48 + Z % 10, U[D++] = 58, F = D;
|
|
76
|
-
let X = q.getSeconds();
|
|
77
|
-
z2[0] = 48 + Math.floor(X / 10), z2[1] = 48 + X % 10, W = 2, V = x;
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
var k = 0;
|
|
81
|
-
function H(x, G, q) {
|
|
82
|
-
let D = 8192 - G;
|
|
83
|
-
if (D <= 0) return G;
|
|
84
|
-
let R = Math.min(q.length, D);
|
|
85
|
-
return x.set(q.subarray(0, R), G), G + R;
|
|
86
|
-
}
|
|
87
|
-
function $(x, G, q) {
|
|
88
|
-
let D = 8192 - G;
|
|
89
|
-
if (D <= 0) return G;
|
|
90
|
-
let R = true, N = q.length;
|
|
91
|
-
for (let M = 0; M < N; M++) if (q.charCodeAt(M) > 127) {
|
|
92
|
-
R = false;
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
if (R) {
|
|
96
|
-
let M = Math.min(N, D);
|
|
97
|
-
for (let Z = 0; Z < M; Z++) x[G + Z] = q.charCodeAt(Z);
|
|
98
|
-
return G + M;
|
|
99
|
-
}
|
|
100
|
-
let P = Q.encodeInto(q, x.subarray(G));
|
|
101
|
-
return G + (P.written ?? 0);
|
|
102
|
-
}
|
|
103
|
-
function A2(x, G, q) {
|
|
104
|
-
if (Number.isNaN(q)) return $(x, G, "NaN");
|
|
105
|
-
if (q === Number.POSITIVE_INFINITY) return $(x, G, "Infinity");
|
|
106
|
-
if (q === Number.NEGATIVE_INFINITY) return $(x, G, "-Infinity");
|
|
107
|
-
let D = G, R = q;
|
|
108
|
-
if (R < 0) D = H(x, D, J.MINUS), R = -R;
|
|
109
|
-
if (R === 0) return H(x, D, J.ZERO);
|
|
110
|
-
if (Number.isInteger(R) && R < Number.MAX_SAFE_INTEGER) {
|
|
111
|
-
let M = Math.floor(R), Z = 0, X = M;
|
|
112
|
-
while (X > 0) Z++, X = Math.floor(X / 10);
|
|
113
|
-
if (D + Z > x.length) return D;
|
|
114
|
-
let Y = D + Z - 1;
|
|
115
|
-
while (M > 0) x[Y--] = 48 + M % 10, M = Math.floor(M / 10);
|
|
116
|
-
return D + Z;
|
|
117
|
-
}
|
|
118
|
-
let N = Q.encodeInto(R.toString(), L), P = Math.min(N.written ?? 0, x.length - G);
|
|
119
|
-
if (P > 0) x.set(L.subarray(0, P), G);
|
|
120
|
-
return G + P;
|
|
121
|
-
}
|
|
122
|
-
function _(x, G, q) {
|
|
123
|
-
if (q === null) return H(x, G, J.NULL);
|
|
124
|
-
if (q === void 0) return H(x, G, J.UNDEFINED);
|
|
125
|
-
switch (typeof q) {
|
|
126
|
-
case "string":
|
|
127
|
-
return $(x, G, q);
|
|
128
|
-
case "number":
|
|
129
|
-
return A2(x, G, q);
|
|
130
|
-
case "boolean":
|
|
131
|
-
return H(x, G, q ? J.TRUE : J.FALSE);
|
|
132
|
-
case "bigint":
|
|
133
|
-
return $(x, G, `${q}`);
|
|
134
|
-
case "symbol":
|
|
135
|
-
return $(x, G, String(q));
|
|
136
|
-
case "function":
|
|
137
|
-
return $(x, G, `${q}`);
|
|
138
|
-
case "object": {
|
|
139
|
-
if (Array.isArray(q)) {
|
|
140
|
-
let X = H(x, G, J.BRACKET_OPEN), Y = q.length;
|
|
141
|
-
for (let O = 0; O < Y; O++) {
|
|
142
|
-
if (O > 0) X = H(x, X, J.COMMA);
|
|
143
|
-
X = _(x, X, q[O]);
|
|
144
|
-
}
|
|
145
|
-
return H(x, X, J.BRACKET_CLOSE);
|
|
146
|
-
}
|
|
147
|
-
let N = q.toString;
|
|
148
|
-
if (typeof N === "function" && N !== Object.prototype.toString) return $(x, G, N.call(q));
|
|
149
|
-
let P = q, M = H(x, G, J.BRACE_OPEN), Z = true;
|
|
150
|
-
for (let X in P) if (Object.hasOwn(P, X)) {
|
|
151
|
-
if (Z === false) M = H(x, M, J.COMMA);
|
|
152
|
-
M = H(x, M, J.QUOTE), M = $(x, M, X), M = H(x, M, J.QUOTE), M = H(x, M, J.COLON), M = _(x, M, P[X]), Z = false;
|
|
153
|
-
}
|
|
154
|
-
return H(x, M, J.BRACE_CLOSE);
|
|
155
|
-
}
|
|
156
|
-
default:
|
|
157
|
-
return $(x, G, `${q}`);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
function E(x, G, q) {
|
|
161
|
-
if (!q) return G;
|
|
162
|
-
let D = G, R = true;
|
|
163
|
-
for (let N in q) {
|
|
164
|
-
if (!R) D = H(x, D, J.SPACE);
|
|
165
|
-
D = $(x, D, N), D = H(x, D, J.EQUALS), D = _(x, D, q[N]), R = false;
|
|
166
|
-
}
|
|
167
|
-
return D;
|
|
168
|
-
}
|
|
169
|
-
function I2(x) {
|
|
170
|
-
let G = K.subarray(0, x);
|
|
171
|
-
process.stderr.write(G);
|
|
172
|
-
}
|
|
173
|
-
function y(x, G) {
|
|
174
|
-
if (-4 < k) return;
|
|
175
|
-
j();
|
|
176
|
-
let q = 0;
|
|
177
|
-
if (q = H(K, q, U.subarray(0, F)), q = H(K, q, z2.subarray(0, W)), q = H(K, q, J.DEBUG), q = $(K, q, x), G) q = H(K, q, J.SPACE), q = E(K, q, G);
|
|
178
|
-
if (q < 8192) q = H(K, q, J.NEWLINE);
|
|
179
|
-
else {
|
|
180
|
-
let D = J.NEWLINE[0];
|
|
181
|
-
if (D !== void 0) K[8191] = D;
|
|
182
|
-
q = 8192;
|
|
183
|
-
}
|
|
184
|
-
I2(q);
|
|
185
|
-
}
|
|
186
|
-
function l(x, G) {
|
|
187
|
-
if (8 < k) return;
|
|
188
|
-
j();
|
|
189
|
-
let q = 0;
|
|
190
|
-
if (q = H(K, q, U.subarray(0, F)), q = H(K, q, z2.subarray(0, W)), q = H(K, q, J.ERROR), q = $(K, q, x), G) q = H(K, q, J.SPACE), q = E(K, q, G);
|
|
191
|
-
if (q < 8192) q = H(K, q, J.NEWLINE);
|
|
192
|
-
else {
|
|
193
|
-
let D = J.NEWLINE[0];
|
|
194
|
-
if (D !== void 0) K[8191] = D;
|
|
195
|
-
q = 8192;
|
|
196
|
-
}
|
|
197
|
-
I2(q);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
12
|
// src/html/sanitize.ts
|
|
201
13
|
var DEFAULT_CONFIG = {
|
|
202
14
|
// HTML content tags
|
|
@@ -525,101 +337,101 @@ function serializeVerbatim(node) {
|
|
|
525
337
|
}).join("");
|
|
526
338
|
return `<${node.tagName}${attrs}>${inner}</${node.tagName}>`;
|
|
527
339
|
}
|
|
528
|
-
var QtiCardinalitySchema = z
|
|
529
|
-
var QtiBaseTypeSchema = z
|
|
530
|
-
var SimpleChoiceSchema = z
|
|
531
|
-
identifier: z
|
|
532
|
-
contentHtml: z
|
|
340
|
+
var QtiCardinalitySchema = z.enum(["single", "multiple", "ordered"]);
|
|
341
|
+
var QtiBaseTypeSchema = z.enum(["identifier", "string", "float", "integer", "boolean", "directedPair", "pair"]);
|
|
342
|
+
var SimpleChoiceSchema = z.object({
|
|
343
|
+
identifier: z.string().min(1),
|
|
344
|
+
contentHtml: z.string(),
|
|
533
345
|
// Allows "" for empty or image-only content
|
|
534
|
-
inlineFeedbackHtml: z
|
|
346
|
+
inlineFeedbackHtml: z.string().optional()
|
|
535
347
|
// Optional per-choice feedback (qti-feedback-inline)
|
|
536
348
|
});
|
|
537
|
-
var InlineChoiceSchema = z
|
|
538
|
-
identifier: z
|
|
539
|
-
contentHtml: z
|
|
349
|
+
var InlineChoiceSchema = z.object({
|
|
350
|
+
identifier: z.string().min(1),
|
|
351
|
+
contentHtml: z.string()
|
|
540
352
|
});
|
|
541
|
-
var ChoiceInteractionCoreSchema = z
|
|
542
|
-
responseIdentifier: z
|
|
543
|
-
shuffle: z
|
|
544
|
-
minChoices: z
|
|
545
|
-
maxChoices: z
|
|
546
|
-
promptHtml: z
|
|
547
|
-
choices: z
|
|
353
|
+
var ChoiceInteractionCoreSchema = z.object({
|
|
354
|
+
responseIdentifier: z.string().min(1),
|
|
355
|
+
shuffle: z.boolean(),
|
|
356
|
+
minChoices: z.number().int().min(0),
|
|
357
|
+
maxChoices: z.number().int().min(1),
|
|
358
|
+
promptHtml: z.string(),
|
|
359
|
+
choices: z.array(SimpleChoiceSchema).min(1)
|
|
548
360
|
});
|
|
549
|
-
var ChoiceInteractionSchema = ChoiceInteractionCoreSchema.extend({ type: z
|
|
550
|
-
var InlineChoiceInteractionCoreSchema = z
|
|
551
|
-
responseIdentifier: z
|
|
552
|
-
shuffle: z
|
|
553
|
-
choices: z
|
|
361
|
+
var ChoiceInteractionSchema = ChoiceInteractionCoreSchema.extend({ type: z.literal("choiceInteraction") });
|
|
362
|
+
var InlineChoiceInteractionCoreSchema = z.object({
|
|
363
|
+
responseIdentifier: z.string().min(1),
|
|
364
|
+
shuffle: z.boolean(),
|
|
365
|
+
choices: z.array(InlineChoiceSchema).min(1)
|
|
554
366
|
});
|
|
555
367
|
var InlineChoiceInteractionSchema = InlineChoiceInteractionCoreSchema.extend({
|
|
556
|
-
type: z
|
|
368
|
+
type: z.literal("inlineChoiceInteraction")
|
|
557
369
|
});
|
|
558
|
-
var TextEntryInteractionCoreSchema = z
|
|
559
|
-
responseIdentifier: z
|
|
560
|
-
expectedLength: z
|
|
561
|
-
placeholderText: z
|
|
562
|
-
patternMask: z
|
|
370
|
+
var TextEntryInteractionCoreSchema = z.object({
|
|
371
|
+
responseIdentifier: z.string().min(1),
|
|
372
|
+
expectedLength: z.number().int().min(0).optional(),
|
|
373
|
+
placeholderText: z.string().optional(),
|
|
374
|
+
patternMask: z.string().optional()
|
|
563
375
|
});
|
|
564
376
|
var TextEntryInteractionSchema = TextEntryInteractionCoreSchema.extend({
|
|
565
|
-
type: z
|
|
377
|
+
type: z.literal("textEntryInteraction")
|
|
566
378
|
});
|
|
567
|
-
var OrderInteractionCoreSchema = z
|
|
568
|
-
responseIdentifier: z
|
|
569
|
-
shuffle: z
|
|
570
|
-
minChoices: z
|
|
571
|
-
maxChoices: z
|
|
379
|
+
var OrderInteractionCoreSchema = z.object({
|
|
380
|
+
responseIdentifier: z.string().min(1),
|
|
381
|
+
shuffle: z.boolean(),
|
|
382
|
+
minChoices: z.number().int().min(0),
|
|
383
|
+
maxChoices: z.number().int().min(0).optional(),
|
|
572
384
|
// 0 or undefined usually means "all"
|
|
573
|
-
orientation: z
|
|
574
|
-
promptHtml: z
|
|
575
|
-
choices: z
|
|
385
|
+
orientation: z.enum(["vertical", "horizontal"]),
|
|
386
|
+
promptHtml: z.string(),
|
|
387
|
+
choices: z.array(SimpleChoiceSchema).min(1)
|
|
576
388
|
});
|
|
577
389
|
var OrderInteractionSchema = OrderInteractionCoreSchema.extend({
|
|
578
|
-
type: z
|
|
390
|
+
type: z.literal("orderInteraction")
|
|
579
391
|
});
|
|
580
|
-
var GapTextSchema = z
|
|
581
|
-
identifier: z
|
|
582
|
-
contentHtml: z
|
|
583
|
-
matchMax: z
|
|
392
|
+
var GapTextSchema = z.object({
|
|
393
|
+
identifier: z.string().min(1),
|
|
394
|
+
contentHtml: z.string(),
|
|
395
|
+
matchMax: z.number().int().min(0)
|
|
584
396
|
// 0 = unlimited
|
|
585
397
|
});
|
|
586
|
-
var GapSchema = z
|
|
587
|
-
identifier: z
|
|
398
|
+
var GapSchema = z.object({
|
|
399
|
+
identifier: z.string().min(1)
|
|
588
400
|
});
|
|
589
|
-
var GapMatchInteractionCoreSchema = z
|
|
590
|
-
responseIdentifier: z
|
|
591
|
-
shuffle: z
|
|
592
|
-
gapTexts: z
|
|
401
|
+
var GapMatchInteractionCoreSchema = z.object({
|
|
402
|
+
responseIdentifier: z.string().min(1),
|
|
403
|
+
shuffle: z.boolean(),
|
|
404
|
+
gapTexts: z.array(GapTextSchema).min(1),
|
|
593
405
|
// Draggable source tokens
|
|
594
|
-
gaps: z
|
|
406
|
+
gaps: z.array(GapSchema).min(1),
|
|
595
407
|
// Drop target placeholders
|
|
596
|
-
contentHtml: z
|
|
408
|
+
contentHtml: z.string()
|
|
597
409
|
// HTML content with gap placeholders
|
|
598
410
|
});
|
|
599
411
|
var GapMatchInteractionSchema = GapMatchInteractionCoreSchema.extend({
|
|
600
|
-
type: z
|
|
412
|
+
type: z.literal("gapMatchInteraction")
|
|
601
413
|
});
|
|
602
|
-
var AssociableChoiceSchema = z
|
|
603
|
-
identifier: z
|
|
604
|
-
matchMax: z
|
|
414
|
+
var AssociableChoiceSchema = z.object({
|
|
415
|
+
identifier: z.string().min(1),
|
|
416
|
+
matchMax: z.number().int().min(0),
|
|
605
417
|
// 0 = unlimited uses
|
|
606
|
-
contentHtml: z
|
|
418
|
+
contentHtml: z.string()
|
|
607
419
|
});
|
|
608
|
-
var MatchInteractionCoreSchema = z
|
|
609
|
-
responseIdentifier: z
|
|
610
|
-
shuffle: z
|
|
611
|
-
maxAssociations: z
|
|
420
|
+
var MatchInteractionCoreSchema = z.object({
|
|
421
|
+
responseIdentifier: z.string().min(1),
|
|
422
|
+
shuffle: z.boolean(),
|
|
423
|
+
maxAssociations: z.number().int().min(0),
|
|
612
424
|
// 0 = unlimited total associations
|
|
613
|
-
sourceChoices: z
|
|
425
|
+
sourceChoices: z.array(AssociableChoiceSchema).min(1),
|
|
614
426
|
// First <qti-simple-match-set>
|
|
615
|
-
targetChoices: z
|
|
427
|
+
targetChoices: z.array(AssociableChoiceSchema).min(1),
|
|
616
428
|
// Second <qti-simple-match-set>
|
|
617
|
-
promptHtml: z
|
|
429
|
+
promptHtml: z.string()
|
|
618
430
|
});
|
|
619
431
|
var MatchInteractionSchema = MatchInteractionCoreSchema.extend({
|
|
620
|
-
type: z
|
|
432
|
+
type: z.literal("matchInteraction")
|
|
621
433
|
});
|
|
622
|
-
var AnyInteractionSchema = z
|
|
434
|
+
var AnyInteractionSchema = z.discriminatedUnion("type", [
|
|
623
435
|
ChoiceInteractionSchema,
|
|
624
436
|
InlineChoiceInteractionSchema,
|
|
625
437
|
TextEntryInteractionSchema,
|
|
@@ -627,149 +439,149 @@ var AnyInteractionSchema = z$1.discriminatedUnion("type", [
|
|
|
627
439
|
GapMatchInteractionSchema,
|
|
628
440
|
MatchInteractionSchema
|
|
629
441
|
]);
|
|
630
|
-
var CorrectResponseSchema = z
|
|
631
|
-
values: z
|
|
442
|
+
var CorrectResponseSchema = z.object({
|
|
443
|
+
values: z.array(z.string().min(1)).min(1)
|
|
632
444
|
});
|
|
633
|
-
var ResponseDeclarationSchema = z
|
|
634
|
-
identifier: z
|
|
445
|
+
var ResponseDeclarationSchema = z.object({
|
|
446
|
+
identifier: z.string().min(1),
|
|
635
447
|
cardinality: QtiCardinalitySchema,
|
|
636
448
|
baseType: QtiBaseTypeSchema,
|
|
637
449
|
correctResponse: CorrectResponseSchema,
|
|
638
450
|
// Optional response mapping for map-response processing (used for summed feedback/score)
|
|
639
|
-
mapping: z
|
|
640
|
-
defaultValue: z
|
|
641
|
-
lowerBound: z
|
|
642
|
-
upperBound: z
|
|
643
|
-
entries: z
|
|
644
|
-
z
|
|
645
|
-
key: z
|
|
646
|
-
value: z
|
|
451
|
+
mapping: z.object({
|
|
452
|
+
defaultValue: z.number().default(0),
|
|
453
|
+
lowerBound: z.number().optional(),
|
|
454
|
+
upperBound: z.number().optional(),
|
|
455
|
+
entries: z.array(
|
|
456
|
+
z.object({
|
|
457
|
+
key: z.string().min(1),
|
|
458
|
+
value: z.number()
|
|
647
459
|
})
|
|
648
460
|
)
|
|
649
461
|
}).optional()
|
|
650
462
|
});
|
|
651
|
-
var OutcomeDefaultValueSchema = z
|
|
652
|
-
value: z
|
|
463
|
+
var OutcomeDefaultValueSchema = z.object({
|
|
464
|
+
value: z.string()
|
|
653
465
|
});
|
|
654
|
-
var OutcomeDeclarationSchema = z
|
|
655
|
-
identifier: z
|
|
466
|
+
var OutcomeDeclarationSchema = z.object({
|
|
467
|
+
identifier: z.string().min(1),
|
|
656
468
|
cardinality: QtiCardinalitySchema,
|
|
657
469
|
baseType: QtiBaseTypeSchema,
|
|
658
470
|
defaultValue: OutcomeDefaultValueSchema.optional()
|
|
659
471
|
});
|
|
660
|
-
var FeedbackBlockSchema = z
|
|
661
|
-
outcomeIdentifier: z
|
|
662
|
-
identifier: z
|
|
663
|
-
showHide: z
|
|
664
|
-
contentHtml: z
|
|
472
|
+
var FeedbackBlockSchema = z.object({
|
|
473
|
+
outcomeIdentifier: z.string().min(1),
|
|
474
|
+
identifier: z.string().min(1),
|
|
475
|
+
showHide: z.enum(["show", "hide"]).default("show"),
|
|
476
|
+
contentHtml: z.string()
|
|
665
477
|
});
|
|
666
|
-
var StimulusBlockSchema = z
|
|
667
|
-
type: z
|
|
668
|
-
html: z
|
|
478
|
+
var StimulusBlockSchema = z.object({
|
|
479
|
+
type: z.literal("stimulus"),
|
|
480
|
+
html: z.string()
|
|
669
481
|
});
|
|
670
|
-
var RichStimulusBlockSchema = z
|
|
671
|
-
type: z
|
|
672
|
-
html: z
|
|
673
|
-
inlineEmbeds: z
|
|
674
|
-
textEmbeds: z
|
|
482
|
+
var RichStimulusBlockSchema = z.object({
|
|
483
|
+
type: z.literal("richStimulus"),
|
|
484
|
+
html: z.string(),
|
|
485
|
+
inlineEmbeds: z.record(z.string(), InlineChoiceInteractionSchema),
|
|
486
|
+
textEmbeds: z.record(z.string(), TextEntryInteractionSchema)
|
|
675
487
|
});
|
|
676
|
-
var InteractionBlockSchema = z
|
|
677
|
-
type: z
|
|
488
|
+
var InteractionBlockSchema = z.object({
|
|
489
|
+
type: z.literal("interaction"),
|
|
678
490
|
interaction: AnyInteractionSchema
|
|
679
491
|
});
|
|
680
|
-
var ItemBodySchema = z
|
|
681
|
-
contentBlocks: z
|
|
682
|
-
feedbackBlocks: z
|
|
492
|
+
var ItemBodySchema = z.object({
|
|
493
|
+
contentBlocks: z.array(z.union([StimulusBlockSchema, RichStimulusBlockSchema, InteractionBlockSchema])).min(1),
|
|
494
|
+
feedbackBlocks: z.array(FeedbackBlockSchema)
|
|
683
495
|
});
|
|
684
|
-
var BaseRuleSchema = z
|
|
685
|
-
type: z
|
|
686
|
-
identifier: z
|
|
687
|
-
value: z
|
|
496
|
+
var BaseRuleSchema = z.object({
|
|
497
|
+
type: z.literal("setOutcomeValue"),
|
|
498
|
+
identifier: z.string(),
|
|
499
|
+
value: z.string()
|
|
688
500
|
});
|
|
689
|
-
var MatchConditionSchema = z
|
|
690
|
-
type: z
|
|
691
|
-
variable: z
|
|
692
|
-
correct: z
|
|
501
|
+
var MatchConditionSchema = z.object({
|
|
502
|
+
type: z.literal("match"),
|
|
503
|
+
variable: z.string(),
|
|
504
|
+
correct: z.literal(true)
|
|
693
505
|
// Match against correct response
|
|
694
506
|
});
|
|
695
|
-
var MatchValueConditionSchema = z
|
|
696
|
-
type: z
|
|
697
|
-
variable: z
|
|
507
|
+
var MatchValueConditionSchema = z.object({
|
|
508
|
+
type: z.literal("matchValue"),
|
|
509
|
+
variable: z.string(),
|
|
698
510
|
// The response identifier from <qti-variable>
|
|
699
|
-
value: z
|
|
511
|
+
value: z.string()
|
|
700
512
|
// The target value from <qti-base-value>
|
|
701
513
|
});
|
|
702
|
-
var StringMatchConditionSchema = z
|
|
703
|
-
type: z
|
|
704
|
-
variable: z
|
|
705
|
-
value: z
|
|
706
|
-
caseSensitive: z
|
|
514
|
+
var StringMatchConditionSchema = z.object({
|
|
515
|
+
type: z.literal("stringMatch"),
|
|
516
|
+
variable: z.string(),
|
|
517
|
+
value: z.string(),
|
|
518
|
+
caseSensitive: z.boolean()
|
|
707
519
|
});
|
|
708
|
-
var MemberConditionSchema = z
|
|
709
|
-
type: z
|
|
710
|
-
variable: z
|
|
711
|
-
value: z
|
|
520
|
+
var MemberConditionSchema = z.object({
|
|
521
|
+
type: z.literal("member"),
|
|
522
|
+
variable: z.string(),
|
|
523
|
+
value: z.string()
|
|
712
524
|
});
|
|
713
|
-
var EqualMapResponseConditionSchema = z
|
|
714
|
-
type: z
|
|
715
|
-
variable: z
|
|
525
|
+
var EqualMapResponseConditionSchema = z.object({
|
|
526
|
+
type: z.literal("equalMapResponse"),
|
|
527
|
+
variable: z.string(),
|
|
716
528
|
// The response identifier from <qti-map-response>
|
|
717
|
-
value: z
|
|
529
|
+
value: z.string()
|
|
718
530
|
// The target sum value from <qti-base-value>
|
|
719
531
|
});
|
|
720
|
-
var AndConditionSchema = z
|
|
721
|
-
type: z
|
|
722
|
-
conditions: z
|
|
532
|
+
var AndConditionSchema = z.object({
|
|
533
|
+
type: z.literal("and"),
|
|
534
|
+
conditions: z.array(z.lazy(() => AnyConditionSchema))
|
|
723
535
|
});
|
|
724
|
-
var OrConditionSchema = z
|
|
725
|
-
type: z
|
|
726
|
-
conditions: z
|
|
536
|
+
var OrConditionSchema = z.object({
|
|
537
|
+
type: z.literal("or"),
|
|
538
|
+
conditions: z.array(z.lazy(() => AnyConditionSchema))
|
|
727
539
|
});
|
|
728
|
-
var NotConditionSchema = z
|
|
729
|
-
type: z
|
|
730
|
-
condition: z
|
|
540
|
+
var NotConditionSchema = z.object({
|
|
541
|
+
type: z.literal("not"),
|
|
542
|
+
condition: z.lazy(() => AnyConditionSchema)
|
|
731
543
|
});
|
|
732
|
-
var AnyConditionSchema = z
|
|
544
|
+
var AnyConditionSchema = z.union([
|
|
733
545
|
MatchConditionSchema,
|
|
734
546
|
MatchValueConditionSchema,
|
|
735
547
|
StringMatchConditionSchema,
|
|
736
548
|
MemberConditionSchema,
|
|
737
|
-
z
|
|
738
|
-
z
|
|
739
|
-
z
|
|
549
|
+
z.lazy(() => AndConditionSchema),
|
|
550
|
+
z.lazy(() => OrConditionSchema),
|
|
551
|
+
z.lazy(() => NotConditionSchema),
|
|
740
552
|
EqualMapResponseConditionSchema
|
|
741
553
|
]);
|
|
742
|
-
var ConditionBranchSchema = z
|
|
554
|
+
var ConditionBranchSchema = z.object({
|
|
743
555
|
condition: AnyConditionSchema.optional(),
|
|
744
|
-
actions: z
|
|
745
|
-
nestedRules: z
|
|
556
|
+
actions: z.array(BaseRuleSchema),
|
|
557
|
+
nestedRules: z.array(z.lazy(() => ResponseRuleSchema)).optional()
|
|
746
558
|
});
|
|
747
|
-
var ResponseRuleSchema = z
|
|
748
|
-
z
|
|
749
|
-
type: z
|
|
750
|
-
branches: z
|
|
559
|
+
var ResponseRuleSchema = z.union([
|
|
560
|
+
z.object({
|
|
561
|
+
type: z.literal("condition"),
|
|
562
|
+
branches: z.array(ConditionBranchSchema)
|
|
751
563
|
}),
|
|
752
|
-
z
|
|
753
|
-
type: z
|
|
564
|
+
z.object({
|
|
565
|
+
type: z.literal("action"),
|
|
754
566
|
action: BaseRuleSchema
|
|
755
567
|
})
|
|
756
568
|
]);
|
|
757
|
-
var ScoringRuleSchema = z
|
|
758
|
-
responseIdentifier: z
|
|
759
|
-
correctScore: z
|
|
760
|
-
incorrectScore: z
|
|
569
|
+
var ScoringRuleSchema = z.object({
|
|
570
|
+
responseIdentifier: z.string().min(1),
|
|
571
|
+
correctScore: z.number(),
|
|
572
|
+
incorrectScore: z.number()
|
|
761
573
|
});
|
|
762
|
-
var ResponseProcessingSchema = z
|
|
763
|
-
rules: z
|
|
574
|
+
var ResponseProcessingSchema = z.object({
|
|
575
|
+
rules: z.array(ResponseRuleSchema),
|
|
764
576
|
scoring: ScoringRuleSchema
|
|
765
577
|
});
|
|
766
|
-
var AssessmentItemSchema = z
|
|
767
|
-
identifier: z
|
|
768
|
-
title: z
|
|
769
|
-
timeDependent: z
|
|
770
|
-
xmlLang: z
|
|
771
|
-
responseDeclarations: z
|
|
772
|
-
outcomeDeclarations: z
|
|
578
|
+
var AssessmentItemSchema = z.object({
|
|
579
|
+
identifier: z.string().min(1),
|
|
580
|
+
title: z.string().min(1),
|
|
581
|
+
timeDependent: z.boolean(),
|
|
582
|
+
xmlLang: z.string().min(1),
|
|
583
|
+
responseDeclarations: z.array(ResponseDeclarationSchema).min(1),
|
|
584
|
+
outcomeDeclarations: z.array(OutcomeDeclarationSchema).min(1),
|
|
773
585
|
itemBody: ItemBodySchema,
|
|
774
586
|
responseProcessing: ResponseProcessingSchema
|
|
775
587
|
});
|
|
@@ -801,7 +613,7 @@ function normalizeNode(rawNode) {
|
|
|
801
613
|
if (textContent != null) {
|
|
802
614
|
return coerceString(textContent);
|
|
803
615
|
}
|
|
804
|
-
const tagName = Object.keys(rawNode).find((
|
|
616
|
+
const tagName = Object.keys(rawNode).find((k) => k !== ":@");
|
|
805
617
|
if (!tagName) return "";
|
|
806
618
|
const attrsValue = rawNode[":@"];
|
|
807
619
|
const attrs = isRecord(attrsValue) ? attrsValue : {};
|
|
@@ -1059,12 +871,12 @@ function extractResponseDeclarations(rootChildren) {
|
|
|
1059
871
|
const cardinalityRaw = coerceString(node.attrs.cardinality);
|
|
1060
872
|
const cardinalityResult = QtiCardinalitySchema.safeParse(cardinalityRaw);
|
|
1061
873
|
if (!cardinalityResult.success) {
|
|
1062
|
-
throw
|
|
874
|
+
throw errors.new(`invalid cardinality '${cardinalityRaw}' in response declaration`);
|
|
1063
875
|
}
|
|
1064
876
|
const baseTypeRaw = coerceString(node.attrs["base-type"]);
|
|
1065
877
|
const baseTypeResult = QtiBaseTypeSchema.safeParse(baseTypeRaw);
|
|
1066
878
|
if (!baseTypeResult.success) {
|
|
1067
|
-
throw
|
|
879
|
+
throw errors.new(`invalid base-type '${baseTypeRaw}' in response declaration`);
|
|
1068
880
|
}
|
|
1069
881
|
return {
|
|
1070
882
|
identifier: coerceString(node.attrs.identifier),
|
|
@@ -1084,12 +896,12 @@ function extractOutcomeDeclarations(rootChildren) {
|
|
|
1084
896
|
const cardinalityRaw = coerceString(node.attrs.cardinality);
|
|
1085
897
|
const cardinalityResult = QtiCardinalitySchema.safeParse(cardinalityRaw);
|
|
1086
898
|
if (!cardinalityResult.success) {
|
|
1087
|
-
throw
|
|
899
|
+
throw errors.new(`invalid cardinality '${cardinalityRaw}' in outcome declaration`);
|
|
1088
900
|
}
|
|
1089
901
|
const baseTypeRaw = coerceString(node.attrs["base-type"]);
|
|
1090
902
|
const baseTypeResult = QtiBaseTypeSchema.safeParse(baseTypeRaw);
|
|
1091
903
|
if (!baseTypeResult.success) {
|
|
1092
|
-
throw
|
|
904
|
+
throw errors.new(`invalid base-type '${baseTypeRaw}' in outcome declaration`);
|
|
1093
905
|
}
|
|
1094
906
|
return {
|
|
1095
907
|
identifier: coerceString(node.attrs.identifier),
|
|
@@ -1306,23 +1118,23 @@ function extractResponseProcessing(rootChildren) {
|
|
|
1306
1118
|
}
|
|
1307
1119
|
function parseAssessmentItemXml(xml) {
|
|
1308
1120
|
if (!xml || typeof xml !== "string") {
|
|
1309
|
-
throw
|
|
1121
|
+
throw errors.new("xml input must be a non-empty string");
|
|
1310
1122
|
}
|
|
1311
1123
|
const parser = createXmlParser();
|
|
1312
|
-
const parseResult =
|
|
1124
|
+
const parseResult = errors.trySync(() => {
|
|
1313
1125
|
return parser.parse(xml, true);
|
|
1314
1126
|
});
|
|
1315
1127
|
if (parseResult.error) {
|
|
1316
|
-
throw
|
|
1128
|
+
throw errors.wrap(parseResult.error, "xml parse");
|
|
1317
1129
|
}
|
|
1318
1130
|
const raw = parseResult.data;
|
|
1319
1131
|
if (!Array.isArray(raw)) {
|
|
1320
|
-
throw
|
|
1132
|
+
throw errors.new("expected xml parser to output an array for preserveOrder");
|
|
1321
1133
|
}
|
|
1322
1134
|
const normalizedTree = raw.map(normalizeNode).filter((n) => typeof n !== "string");
|
|
1323
1135
|
const rootNode = normalizedTree.find((n) => n.tagName.endsWith("assessment-item"));
|
|
1324
1136
|
if (!rootNode) {
|
|
1325
|
-
throw
|
|
1137
|
+
throw errors.new("qti assessment item not found in xml document");
|
|
1326
1138
|
}
|
|
1327
1139
|
const rootChildren = rootNode.children.filter((c) => typeof c !== "string");
|
|
1328
1140
|
const itemBodyNode = rootChildren.find((n) => n.tagName === "qti-item-body");
|
|
@@ -1394,204 +1206,32 @@ function parseAssessmentItemXml(xml) {
|
|
|
1394
1206
|
const validation = AssessmentItemSchema.safeParse(normalizedItem);
|
|
1395
1207
|
if (!validation.success) {
|
|
1396
1208
|
const errorDetails = validation.error.issues.map((err) => `${err.path.join(".")}: ${err.message}`).join("; ");
|
|
1397
|
-
throw
|
|
1209
|
+
throw errors.new(`qti item validation: ${errorDetails}`);
|
|
1398
1210
|
}
|
|
1399
1211
|
return validation.data;
|
|
1400
1212
|
}
|
|
1401
1213
|
|
|
1402
|
-
// src/evaluator.ts
|
|
1403
|
-
function normalizeString(str, caseSensitive) {
|
|
1404
|
-
const s = (str ?? "").trim();
|
|
1405
|
-
return caseSensitive ? s : s.toLowerCase();
|
|
1406
|
-
}
|
|
1407
|
-
function checkCondition(condition, item, responses, responseResults) {
|
|
1408
|
-
if (condition.type === "and") {
|
|
1409
|
-
const results = condition.conditions.map((c) => checkCondition(c, item, responses, responseResults));
|
|
1410
|
-
const allTrue = results.every((r) => r === true);
|
|
1411
|
-
y("qti evaluator: checking AND condition", {
|
|
1412
|
-
numConditions: condition.conditions.length,
|
|
1413
|
-
results,
|
|
1414
|
-
allTrue
|
|
1415
|
-
});
|
|
1416
|
-
return allTrue;
|
|
1417
|
-
}
|
|
1418
|
-
if (condition.type === "or") {
|
|
1419
|
-
const results = condition.conditions.map((c) => checkCondition(c, item, responses, responseResults));
|
|
1420
|
-
const anyTrue = results.some((r) => r === true);
|
|
1421
|
-
y("qti evaluator: checking OR condition", {
|
|
1422
|
-
numConditions: condition.conditions.length,
|
|
1423
|
-
results,
|
|
1424
|
-
anyTrue
|
|
1425
|
-
});
|
|
1426
|
-
return anyTrue;
|
|
1427
|
-
}
|
|
1428
|
-
if (condition.type === "not") {
|
|
1429
|
-
const result = checkCondition(condition.condition, item, responses, responseResults);
|
|
1430
|
-
y("qti evaluator: checking NOT condition", { result, negated: !result });
|
|
1431
|
-
return !result;
|
|
1432
|
-
}
|
|
1433
|
-
if (condition.type === "match") {
|
|
1434
|
-
const result = responseResults[condition.variable];
|
|
1435
|
-
y("qti evaluator: checking match condition", {
|
|
1436
|
-
variable: condition.variable,
|
|
1437
|
-
isCorrect: result,
|
|
1438
|
-
matches: result === true
|
|
1439
|
-
});
|
|
1440
|
-
return result === true;
|
|
1441
|
-
}
|
|
1442
|
-
if (condition.type === "matchValue") {
|
|
1443
|
-
const userResponse = responses[condition.variable];
|
|
1444
|
-
let userValues;
|
|
1445
|
-
if (Array.isArray(userResponse)) {
|
|
1446
|
-
userValues = userResponse;
|
|
1447
|
-
} else if (userResponse) {
|
|
1448
|
-
userValues = [userResponse];
|
|
1449
|
-
} else {
|
|
1450
|
-
userValues = [];
|
|
1451
|
-
}
|
|
1452
|
-
const matches = userValues.includes(condition.value);
|
|
1453
|
-
y("qti evaluator: checking matchValue condition", {
|
|
1454
|
-
variable: condition.variable,
|
|
1455
|
-
targetValue: condition.value,
|
|
1456
|
-
userValues,
|
|
1457
|
-
matches
|
|
1458
|
-
});
|
|
1459
|
-
return matches;
|
|
1460
|
-
}
|
|
1461
|
-
if (condition.type === "stringMatch") {
|
|
1462
|
-
const userResponse = responses[condition.variable];
|
|
1463
|
-
const val = Array.isArray(userResponse) ? userResponse[0] : userResponse;
|
|
1464
|
-
if (!val) return false;
|
|
1465
|
-
const u = normalizeString(String(val), condition.caseSensitive);
|
|
1466
|
-
const t = normalizeString(condition.value, condition.caseSensitive);
|
|
1467
|
-
const matches = u === t;
|
|
1468
|
-
y("qti evaluator: checking stringMatch", {
|
|
1469
|
-
variable: condition.variable,
|
|
1470
|
-
caseSensitive: condition.caseSensitive,
|
|
1471
|
-
matches
|
|
1472
|
-
});
|
|
1473
|
-
return matches;
|
|
1474
|
-
}
|
|
1475
|
-
if (condition.type === "member") {
|
|
1476
|
-
const userResponse = responses[condition.variable];
|
|
1477
|
-
let userValues;
|
|
1478
|
-
if (Array.isArray(userResponse)) {
|
|
1479
|
-
userValues = userResponse;
|
|
1480
|
-
} else if (userResponse) {
|
|
1481
|
-
userValues = [userResponse];
|
|
1482
|
-
} else {
|
|
1483
|
-
userValues = [];
|
|
1484
|
-
}
|
|
1485
|
-
const matches = userValues.includes(condition.value);
|
|
1486
|
-
y("qti evaluator: checking member", {
|
|
1487
|
-
variable: condition.variable,
|
|
1488
|
-
target: condition.value,
|
|
1489
|
-
userValues,
|
|
1490
|
-
matches
|
|
1491
|
-
});
|
|
1492
|
-
return matches;
|
|
1493
|
-
}
|
|
1494
|
-
if (condition.type === "equalMapResponse") {
|
|
1495
|
-
const responseId = condition.variable;
|
|
1496
|
-
const userResponse = responses[responseId];
|
|
1497
|
-
const responseDecl = item.responseDeclarations.find((rd) => rd.identifier === responseId);
|
|
1498
|
-
if (!userResponse || !responseDecl?.mapping) return false;
|
|
1499
|
-
const userValues = Array.isArray(userResponse) ? userResponse : [userResponse];
|
|
1500
|
-
const valueMap = new Map(responseDecl.mapping.entries.map((e) => [e.key, e.value]));
|
|
1501
|
-
let sum = 0;
|
|
1502
|
-
for (const val of userValues) {
|
|
1503
|
-
sum += valueMap.get(val) ?? responseDecl.mapping.defaultValue;
|
|
1504
|
-
}
|
|
1505
|
-
const target = Number(condition.value);
|
|
1506
|
-
const diff = Math.abs(sum - target);
|
|
1507
|
-
const matches = diff < 1e-4;
|
|
1508
|
-
y("qti evaluator: checking equalMapResponse", {
|
|
1509
|
-
responseId,
|
|
1510
|
-
userValues,
|
|
1511
|
-
sum,
|
|
1512
|
-
target: condition.value,
|
|
1513
|
-
matches
|
|
1514
|
-
});
|
|
1515
|
-
return matches;
|
|
1516
|
-
}
|
|
1517
|
-
return false;
|
|
1518
|
-
}
|
|
1519
|
-
function evaluateRule(rule, item, responses, responseResults) {
|
|
1520
|
-
const feedbackIds = [];
|
|
1521
|
-
if (rule.type === "action") {
|
|
1522
|
-
const action = rule.action;
|
|
1523
|
-
if (action.type === "setOutcomeValue" && (action.identifier === "FEEDBACK__OVERALL" || action.identifier === "FEEDBACK__PEDAGOGY" || action.identifier === "FEEDBACK")) {
|
|
1524
|
-
y("qti evaluator: executing action", { id: action.identifier, value: action.value });
|
|
1525
|
-
feedbackIds.push(action.value);
|
|
1526
|
-
}
|
|
1527
|
-
return feedbackIds;
|
|
1528
|
-
}
|
|
1529
|
-
if (rule.type === "condition") {
|
|
1530
|
-
for (const branch of rule.branches) {
|
|
1531
|
-
const isMatch = branch.condition ? checkCondition(branch.condition, item, responses, responseResults) : true;
|
|
1532
|
-
y("qti evaluator: evaluating branch", {
|
|
1533
|
-
hasCondition: !!branch.condition,
|
|
1534
|
-
isMatch
|
|
1535
|
-
});
|
|
1536
|
-
if (isMatch) {
|
|
1537
|
-
const feedbackActions = branch.actions.filter(
|
|
1538
|
-
(r) => r.type === "setOutcomeValue" && (r.identifier === "FEEDBACK__OVERALL" || r.identifier === "FEEDBACK__PEDAGOGY" || r.identifier === "FEEDBACK")
|
|
1539
|
-
);
|
|
1540
|
-
for (const action of feedbackActions) {
|
|
1541
|
-
feedbackIds.push(action.value);
|
|
1542
|
-
}
|
|
1543
|
-
if (branch.nestedRules) {
|
|
1544
|
-
for (const nested of branch.nestedRules) {
|
|
1545
|
-
const nestedIds = evaluateRule(nested, item, responses, responseResults);
|
|
1546
|
-
feedbackIds.push(...nestedIds);
|
|
1547
|
-
}
|
|
1548
|
-
}
|
|
1549
|
-
break;
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
}
|
|
1553
|
-
return feedbackIds;
|
|
1554
|
-
}
|
|
1555
|
-
function evaluateFeedbackIdentifiers(item, responses, responseResults) {
|
|
1556
|
-
const processing = item.responseProcessing;
|
|
1557
|
-
if (!processing) {
|
|
1558
|
-
y("qti evaluator: no response processing found");
|
|
1559
|
-
return [];
|
|
1560
|
-
}
|
|
1561
|
-
y("qti evaluator: starting evaluation", {
|
|
1562
|
-
numRules: processing.rules.length,
|
|
1563
|
-
responseResults
|
|
1564
|
-
});
|
|
1565
|
-
const allFeedbackIds = [];
|
|
1566
|
-
for (const rule of processing.rules) {
|
|
1567
|
-
const ids = evaluateRule(rule, item, responses, responseResults);
|
|
1568
|
-
allFeedbackIds.push(...ids);
|
|
1569
|
-
}
|
|
1570
|
-
y("qti evaluator: selected feedback identifiers", { feedbackIds: allFeedbackIds });
|
|
1571
|
-
return allFeedbackIds;
|
|
1572
|
-
}
|
|
1573
|
-
|
|
1574
1214
|
// src/actions/internal/display.ts
|
|
1575
1215
|
function shuffleArray(items) {
|
|
1576
1216
|
const arr = items.slice();
|
|
1577
1217
|
for (let i = arr.length - 1; i > 0; i--) {
|
|
1578
|
-
const
|
|
1218
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
1579
1219
|
const vi = arr[i];
|
|
1580
|
-
const vj = arr[
|
|
1220
|
+
const vj = arr[j];
|
|
1581
1221
|
if (vi === void 0 || vj === void 0) {
|
|
1582
1222
|
continue;
|
|
1583
1223
|
}
|
|
1584
1224
|
arr[i] = vj;
|
|
1585
|
-
arr[
|
|
1225
|
+
arr[j] = vi;
|
|
1586
1226
|
}
|
|
1587
1227
|
return arr;
|
|
1588
1228
|
}
|
|
1589
1229
|
function buildDisplayModelFromXml(qtiXml) {
|
|
1590
|
-
|
|
1591
|
-
const parseResult =
|
|
1230
|
+
logger2.debug("qti build display model", {});
|
|
1231
|
+
const parseResult = errors.trySync(() => parseAssessmentItemXml(qtiXml));
|
|
1592
1232
|
if (parseResult.error) {
|
|
1593
|
-
|
|
1594
|
-
throw
|
|
1233
|
+
logger2.error("qti parse failed", { error: parseResult.error });
|
|
1234
|
+
throw errors.wrap(parseResult.error, "qti parse");
|
|
1595
1235
|
}
|
|
1596
1236
|
const item = parseResult.data;
|
|
1597
1237
|
const shape = {};
|
|
@@ -1748,17 +1388,187 @@ function buildDisplayModelFromXml(qtiXml) {
|
|
|
1748
1388
|
async function buildDisplayModel(qtiXml) {
|
|
1749
1389
|
return buildDisplayModelFromXml(qtiXml);
|
|
1750
1390
|
}
|
|
1391
|
+
function normalizeString(str, caseSensitive) {
|
|
1392
|
+
const s = (str ?? "").trim();
|
|
1393
|
+
return caseSensitive ? s : s.toLowerCase();
|
|
1394
|
+
}
|
|
1395
|
+
function checkCondition(condition, item, responses, responseResults) {
|
|
1396
|
+
if (condition.type === "and") {
|
|
1397
|
+
const results = condition.conditions.map((c) => checkCondition(c, item, responses, responseResults));
|
|
1398
|
+
const allTrue = results.every((r) => r === true);
|
|
1399
|
+
logger2.debug("qti evaluator: checking AND condition", {
|
|
1400
|
+
numConditions: condition.conditions.length,
|
|
1401
|
+
results,
|
|
1402
|
+
allTrue
|
|
1403
|
+
});
|
|
1404
|
+
return allTrue;
|
|
1405
|
+
}
|
|
1406
|
+
if (condition.type === "or") {
|
|
1407
|
+
const results = condition.conditions.map((c) => checkCondition(c, item, responses, responseResults));
|
|
1408
|
+
const anyTrue = results.some((r) => r === true);
|
|
1409
|
+
logger2.debug("qti evaluator: checking OR condition", {
|
|
1410
|
+
numConditions: condition.conditions.length,
|
|
1411
|
+
results,
|
|
1412
|
+
anyTrue
|
|
1413
|
+
});
|
|
1414
|
+
return anyTrue;
|
|
1415
|
+
}
|
|
1416
|
+
if (condition.type === "not") {
|
|
1417
|
+
const result = checkCondition(condition.condition, item, responses, responseResults);
|
|
1418
|
+
logger2.debug("qti evaluator: checking NOT condition", { result, negated: !result });
|
|
1419
|
+
return !result;
|
|
1420
|
+
}
|
|
1421
|
+
if (condition.type === "match") {
|
|
1422
|
+
const result = responseResults[condition.variable];
|
|
1423
|
+
logger2.debug("qti evaluator: checking match condition", {
|
|
1424
|
+
variable: condition.variable,
|
|
1425
|
+
isCorrect: result,
|
|
1426
|
+
matches: result === true
|
|
1427
|
+
});
|
|
1428
|
+
return result === true;
|
|
1429
|
+
}
|
|
1430
|
+
if (condition.type === "matchValue") {
|
|
1431
|
+
const userResponse = responses[condition.variable];
|
|
1432
|
+
let userValues;
|
|
1433
|
+
if (Array.isArray(userResponse)) {
|
|
1434
|
+
userValues = userResponse;
|
|
1435
|
+
} else if (userResponse) {
|
|
1436
|
+
userValues = [userResponse];
|
|
1437
|
+
} else {
|
|
1438
|
+
userValues = [];
|
|
1439
|
+
}
|
|
1440
|
+
const matches = userValues.includes(condition.value);
|
|
1441
|
+
logger2.debug("qti evaluator: checking matchValue condition", {
|
|
1442
|
+
variable: condition.variable,
|
|
1443
|
+
targetValue: condition.value,
|
|
1444
|
+
userValues,
|
|
1445
|
+
matches
|
|
1446
|
+
});
|
|
1447
|
+
return matches;
|
|
1448
|
+
}
|
|
1449
|
+
if (condition.type === "stringMatch") {
|
|
1450
|
+
const userResponse = responses[condition.variable];
|
|
1451
|
+
const val = Array.isArray(userResponse) ? userResponse[0] : userResponse;
|
|
1452
|
+
if (!val) return false;
|
|
1453
|
+
const u = normalizeString(String(val), condition.caseSensitive);
|
|
1454
|
+
const t = normalizeString(condition.value, condition.caseSensitive);
|
|
1455
|
+
const matches = u === t;
|
|
1456
|
+
logger2.debug("qti evaluator: checking stringMatch", {
|
|
1457
|
+
variable: condition.variable,
|
|
1458
|
+
caseSensitive: condition.caseSensitive,
|
|
1459
|
+
matches
|
|
1460
|
+
});
|
|
1461
|
+
return matches;
|
|
1462
|
+
}
|
|
1463
|
+
if (condition.type === "member") {
|
|
1464
|
+
const userResponse = responses[condition.variable];
|
|
1465
|
+
let userValues;
|
|
1466
|
+
if (Array.isArray(userResponse)) {
|
|
1467
|
+
userValues = userResponse;
|
|
1468
|
+
} else if (userResponse) {
|
|
1469
|
+
userValues = [userResponse];
|
|
1470
|
+
} else {
|
|
1471
|
+
userValues = [];
|
|
1472
|
+
}
|
|
1473
|
+
const matches = userValues.includes(condition.value);
|
|
1474
|
+
logger2.debug("qti evaluator: checking member", {
|
|
1475
|
+
variable: condition.variable,
|
|
1476
|
+
target: condition.value,
|
|
1477
|
+
userValues,
|
|
1478
|
+
matches
|
|
1479
|
+
});
|
|
1480
|
+
return matches;
|
|
1481
|
+
}
|
|
1482
|
+
if (condition.type === "equalMapResponse") {
|
|
1483
|
+
const responseId = condition.variable;
|
|
1484
|
+
const userResponse = responses[responseId];
|
|
1485
|
+
const responseDecl = item.responseDeclarations.find((rd) => rd.identifier === responseId);
|
|
1486
|
+
if (!userResponse || !responseDecl?.mapping) return false;
|
|
1487
|
+
const userValues = Array.isArray(userResponse) ? userResponse : [userResponse];
|
|
1488
|
+
const valueMap = new Map(responseDecl.mapping.entries.map((e) => [e.key, e.value]));
|
|
1489
|
+
let sum = 0;
|
|
1490
|
+
for (const val of userValues) {
|
|
1491
|
+
sum += valueMap.get(val) ?? responseDecl.mapping.defaultValue;
|
|
1492
|
+
}
|
|
1493
|
+
const target = Number(condition.value);
|
|
1494
|
+
const diff = Math.abs(sum - target);
|
|
1495
|
+
const matches = diff < 1e-4;
|
|
1496
|
+
logger2.debug("qti evaluator: checking equalMapResponse", {
|
|
1497
|
+
responseId,
|
|
1498
|
+
userValues,
|
|
1499
|
+
sum,
|
|
1500
|
+
target: condition.value,
|
|
1501
|
+
matches
|
|
1502
|
+
});
|
|
1503
|
+
return matches;
|
|
1504
|
+
}
|
|
1505
|
+
return false;
|
|
1506
|
+
}
|
|
1507
|
+
function evaluateRule(rule, item, responses, responseResults) {
|
|
1508
|
+
const feedbackIds = [];
|
|
1509
|
+
if (rule.type === "action") {
|
|
1510
|
+
const action = rule.action;
|
|
1511
|
+
if (action.type === "setOutcomeValue" && (action.identifier === "FEEDBACK__OVERALL" || action.identifier === "FEEDBACK__PEDAGOGY" || action.identifier === "FEEDBACK")) {
|
|
1512
|
+
logger2.debug("qti evaluator: executing action", { id: action.identifier, value: action.value });
|
|
1513
|
+
feedbackIds.push(action.value);
|
|
1514
|
+
}
|
|
1515
|
+
return feedbackIds;
|
|
1516
|
+
}
|
|
1517
|
+
if (rule.type === "condition") {
|
|
1518
|
+
for (const branch of rule.branches) {
|
|
1519
|
+
const isMatch = branch.condition ? checkCondition(branch.condition, item, responses, responseResults) : true;
|
|
1520
|
+
logger2.debug("qti evaluator: evaluating branch", {
|
|
1521
|
+
hasCondition: !!branch.condition,
|
|
1522
|
+
isMatch
|
|
1523
|
+
});
|
|
1524
|
+
if (isMatch) {
|
|
1525
|
+
const feedbackActions = branch.actions.filter(
|
|
1526
|
+
(r) => r.type === "setOutcomeValue" && (r.identifier === "FEEDBACK__OVERALL" || r.identifier === "FEEDBACK__PEDAGOGY" || r.identifier === "FEEDBACK")
|
|
1527
|
+
);
|
|
1528
|
+
for (const action of feedbackActions) {
|
|
1529
|
+
feedbackIds.push(action.value);
|
|
1530
|
+
}
|
|
1531
|
+
if (branch.nestedRules) {
|
|
1532
|
+
for (const nested of branch.nestedRules) {
|
|
1533
|
+
const nestedIds = evaluateRule(nested, item, responses, responseResults);
|
|
1534
|
+
feedbackIds.push(...nestedIds);
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
break;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
return feedbackIds;
|
|
1542
|
+
}
|
|
1543
|
+
function evaluateFeedbackIdentifiers(item, responses, responseResults) {
|
|
1544
|
+
const processing = item.responseProcessing;
|
|
1545
|
+
if (!processing) {
|
|
1546
|
+
logger2.debug("qti evaluator: no response processing found");
|
|
1547
|
+
return [];
|
|
1548
|
+
}
|
|
1549
|
+
logger2.debug("qti evaluator: starting evaluation", {
|
|
1550
|
+
numRules: processing.rules.length,
|
|
1551
|
+
responseResults
|
|
1552
|
+
});
|
|
1553
|
+
const allFeedbackIds = [];
|
|
1554
|
+
for (const rule of processing.rules) {
|
|
1555
|
+
const ids = evaluateRule(rule, item, responses, responseResults);
|
|
1556
|
+
allFeedbackIds.push(...ids);
|
|
1557
|
+
}
|
|
1558
|
+
logger2.debug("qti evaluator: selected feedback identifiers", { feedbackIds: allFeedbackIds });
|
|
1559
|
+
return allFeedbackIds;
|
|
1560
|
+
}
|
|
1751
1561
|
|
|
1752
1562
|
// src/actions/internal/validate.ts
|
|
1753
1563
|
function validateResponsesFromXml(qtiXml, responses) {
|
|
1754
|
-
|
|
1755
|
-
const parseResult =
|
|
1564
|
+
logger2.debug("qti validate secure", {});
|
|
1565
|
+
const parseResult = errors.trySync(() => parseAssessmentItemXml(qtiXml));
|
|
1756
1566
|
if (parseResult.error) {
|
|
1757
|
-
|
|
1758
|
-
throw
|
|
1567
|
+
logger2.error("qti parse failed during validate", { error: parseResult.error });
|
|
1568
|
+
throw errors.wrap(parseResult.error, "qti parse");
|
|
1759
1569
|
}
|
|
1760
1570
|
const item = parseResult.data;
|
|
1761
|
-
|
|
1571
|
+
logger2.debug("qti validate: parsed item", {
|
|
1762
1572
|
title: item.title,
|
|
1763
1573
|
responseCount: Object.keys(responses).length
|
|
1764
1574
|
});
|
|
@@ -1772,7 +1582,7 @@ function validateResponsesFromXml(qtiXml, responses) {
|
|
|
1772
1582
|
if (user == null) {
|
|
1773
1583
|
allCorrect = false;
|
|
1774
1584
|
responseCorrectness[respId] = false;
|
|
1775
|
-
|
|
1585
|
+
logger2.debug("qti validate: missing response", { responseId: respId });
|
|
1776
1586
|
continue;
|
|
1777
1587
|
}
|
|
1778
1588
|
const userVals = Array.isArray(user) ? user.filter((v) => typeof v === "string") : [user];
|
|
@@ -1780,7 +1590,7 @@ function validateResponsesFromXml(qtiXml, responses) {
|
|
|
1780
1590
|
let isCorrect = false;
|
|
1781
1591
|
if (rd.cardinality === "ordered") {
|
|
1782
1592
|
isCorrect = userVals.length === correctVals.length && userVals.every((val, index) => val === correctVals[index]);
|
|
1783
|
-
|
|
1593
|
+
logger2.debug("qti validate: ordered check", {
|
|
1784
1594
|
responseId: respId,
|
|
1785
1595
|
userVals,
|
|
1786
1596
|
correctVals,
|
|
@@ -1790,7 +1600,7 @@ function validateResponsesFromXml(qtiXml, responses) {
|
|
|
1790
1600
|
const correctSet2 = new Set(correctVals);
|
|
1791
1601
|
const userSet = new Set(userVals);
|
|
1792
1602
|
isCorrect = userSet.size === correctSet2.size && [...userSet].every((v) => correctSet2.has(v));
|
|
1793
|
-
|
|
1603
|
+
logger2.debug("qti validate: directedPair check", {
|
|
1794
1604
|
responseId: respId,
|
|
1795
1605
|
userVals,
|
|
1796
1606
|
correctVals,
|
|
@@ -1808,7 +1618,7 @@ function validateResponsesFromXml(qtiXml, responses) {
|
|
|
1808
1618
|
});
|
|
1809
1619
|
}
|
|
1810
1620
|
}
|
|
1811
|
-
|
|
1621
|
+
logger2.debug("qti validate: numeric check", {
|
|
1812
1622
|
responseId: respId,
|
|
1813
1623
|
baseType: rd.baseType,
|
|
1814
1624
|
userVal: userVals[0],
|
|
@@ -1818,7 +1628,7 @@ function validateResponsesFromXml(qtiXml, responses) {
|
|
|
1818
1628
|
} else if (rd.cardinality === "single") {
|
|
1819
1629
|
const userVal = userVals[0];
|
|
1820
1630
|
isCorrect = userVal != null && correctVals.includes(userVal);
|
|
1821
|
-
|
|
1631
|
+
logger2.debug("qti validate: single cardinality check", {
|
|
1822
1632
|
responseId: respId,
|
|
1823
1633
|
userVal,
|
|
1824
1634
|
correctVals,
|
|
@@ -1828,7 +1638,7 @@ function validateResponsesFromXml(qtiXml, responses) {
|
|
|
1828
1638
|
const correctSet2 = new Set(correctVals);
|
|
1829
1639
|
const userSet = new Set(userVals);
|
|
1830
1640
|
isCorrect = userSet.size === correctSet2.size && [...userSet].every((v) => correctSet2.has(v));
|
|
1831
|
-
|
|
1641
|
+
logger2.debug("qti validate: multiple cardinality check", {
|
|
1832
1642
|
responseId: respId,
|
|
1833
1643
|
userVals,
|
|
1834
1644
|
correctVals,
|
|
@@ -1837,7 +1647,7 @@ function validateResponsesFromXml(qtiXml, responses) {
|
|
|
1837
1647
|
}
|
|
1838
1648
|
if (!isCorrect) allCorrect = false;
|
|
1839
1649
|
responseCorrectness[respId] = isCorrect;
|
|
1840
|
-
|
|
1650
|
+
logger2.debug("qti validate: evaluated response", {
|
|
1841
1651
|
responseId: respId,
|
|
1842
1652
|
selected: userVals,
|
|
1843
1653
|
isCorrect
|
|
@@ -1861,7 +1671,7 @@ function validateResponsesFromXml(qtiXml, responses) {
|
|
|
1861
1671
|
isCorrect: allCorrect,
|
|
1862
1672
|
messageHtml: feedbackHtmlParts.join("")
|
|
1863
1673
|
};
|
|
1864
|
-
|
|
1674
|
+
logger2.debug("qti validate: overall feedback via response processing rules", {
|
|
1865
1675
|
isCorrect: allCorrect,
|
|
1866
1676
|
feedbackIds: feedbackIdentifiers,
|
|
1867
1677
|
numBlocks: feedbackHtmlParts.length
|
|
@@ -1874,7 +1684,7 @@ function validateResponsesFromXml(qtiXml, responses) {
|
|
|
1874
1684
|
);
|
|
1875
1685
|
if (fallbackBlock) {
|
|
1876
1686
|
overallFeedback = { isCorrect: allCorrect, messageHtml: sanitizeHtml(fallbackBlock.contentHtml) };
|
|
1877
|
-
|
|
1687
|
+
logger2.debug("qti validate: overall feedback via fallback block", {
|
|
1878
1688
|
isCorrect: allCorrect,
|
|
1879
1689
|
blockId: fallbackBlock.identifier
|
|
1880
1690
|
});
|