@longsightgroup/qti3-core 0.1.2 → 0.2.1
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 +1 -0
- package/dist/catalog.d.ts +32 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +126 -0
- package/dist/catalog.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +225 -3
- package/dist/parser.js.map +1 -1
- package/dist/session.d.ts +3 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +68 -3
- package/dist/session.js.map +1 -1
- package/dist/support.js +7 -1
- package/dist/support.js.map +1 -1
- package/dist/tts.d.ts +61 -0
- package/dist/tts.d.ts.map +1 -0
- package/dist/tts.js +368 -0
- package/dist/tts.js.map +1 -0
- package/dist/types.d.ts +44 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +200 -3
- package/dist/validation.js.map +1 -1
- package/package.json +1 -1
- package/src/catalog.ts +193 -0
- package/src/index.ts +35 -0
- package/src/parser.ts +266 -3
- package/src/session.ts +90 -0
- package/src/support.ts +9 -1
- package/src/tts.ts +555 -0
- package/src/types.ts +52 -0
- package/src/validation.ts +229 -8
package/src/session.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
QtiDiagnostic,
|
|
6
6
|
QtiDocument,
|
|
7
7
|
QtiModalFeedback,
|
|
8
|
+
QtiPortableCustomStateValue,
|
|
8
9
|
QtiProcessingExpression,
|
|
9
10
|
QtiRecordValue,
|
|
10
11
|
QtiResponseCondition,
|
|
@@ -49,6 +50,8 @@ export interface QtiItemSession {
|
|
|
49
50
|
readonly item: QtiAssessmentItem;
|
|
50
51
|
correctResponses(): Record<string, QtiValue>;
|
|
51
52
|
respond(identifier: string, value: QtiValue): void;
|
|
53
|
+
setInteractionState(identifier: string, state: QtiPortableCustomStateValue): void;
|
|
54
|
+
interactionState(identifier: string): QtiPortableCustomStateValue | undefined;
|
|
52
55
|
setStatus(status: QtiAttemptStatus): void;
|
|
53
56
|
score(): QtiScoreResult;
|
|
54
57
|
serialize(): QtiAttemptStateV1;
|
|
@@ -76,14 +79,24 @@ export function createItemSession(
|
|
|
76
79
|
const priorResponses = cloneValueRecord(priorState?.responses ?? {});
|
|
77
80
|
const priorOutcomes = cloneValueRecord(priorState?.outcomes ?? {});
|
|
78
81
|
const priorTemplateValues = cloneValueRecord(priorState?.templateValues ?? {});
|
|
82
|
+
const priorInteractionStates = clonePortableCustomStateRecord(
|
|
83
|
+
priorState?.interactionStates ?? {},
|
|
84
|
+
);
|
|
79
85
|
let validationMessages = cloneDiagnostics(priorState?.validationMessages ?? []);
|
|
80
86
|
const responses: Record<string, QtiValue> = {};
|
|
81
87
|
const outcomes: Record<string, QtiValue> = {};
|
|
82
88
|
const templateValues: Record<string, QtiValue> = {};
|
|
89
|
+
const interactionStates: Record<string, QtiPortableCustomStateValue> = {};
|
|
83
90
|
const correctResponses: Record<string, QtiValue> = {};
|
|
84
91
|
let status: QtiAttemptStatus = priorState?.status ?? "initialized";
|
|
85
92
|
const random = seededRandom(options.randomSeed ?? document.item.identifier);
|
|
86
93
|
const customOperators = options.customOperators ?? {};
|
|
94
|
+
const portableCustomResponseIdentifiers = new Set(
|
|
95
|
+
document.item.interactions
|
|
96
|
+
.filter((interaction) => interaction.type === "portableCustom")
|
|
97
|
+
.map((interaction) => interaction.responseIdentifier)
|
|
98
|
+
.filter((identifier): identifier is string => Boolean(identifier)),
|
|
99
|
+
);
|
|
87
100
|
|
|
88
101
|
for (const declaration of document.item.responseDeclarations) {
|
|
89
102
|
correctResponses[declaration.identifier] = cloneValue(declaration.correctResponse);
|
|
@@ -132,6 +145,7 @@ export function createItemSession(
|
|
|
132
145
|
const defaultOutcomes = cloneValueRecord(outcomes);
|
|
133
146
|
Object.assign(responses, priorResponses);
|
|
134
147
|
Object.assign(outcomes, priorOutcomes);
|
|
148
|
+
Object.assign(interactionStates, priorInteractionStates);
|
|
135
149
|
|
|
136
150
|
return {
|
|
137
151
|
item: document.item,
|
|
@@ -143,6 +157,18 @@ export function createItemSession(
|
|
|
143
157
|
validationMessages = [];
|
|
144
158
|
startAttempt();
|
|
145
159
|
},
|
|
160
|
+
setInteractionState(identifier: string, state: QtiPortableCustomStateValue) {
|
|
161
|
+
if (!portableCustomResponseIdentifiers.has(identifier)) {
|
|
162
|
+
throw new Error(`Cannot set interaction state for non-PCI response ${identifier}.`);
|
|
163
|
+
}
|
|
164
|
+
interactionStates[identifier] = clonePortableCustomState(state);
|
|
165
|
+
validationMessages = [];
|
|
166
|
+
startAttempt();
|
|
167
|
+
},
|
|
168
|
+
interactionState(identifier: string) {
|
|
169
|
+
const state = interactionStates[identifier];
|
|
170
|
+
return state === undefined ? undefined : clonePortableCustomState(state);
|
|
171
|
+
},
|
|
146
172
|
setStatus(nextStatus: QtiAttemptStatus) {
|
|
147
173
|
status = nextStatus;
|
|
148
174
|
},
|
|
@@ -173,6 +199,7 @@ export function createItemSession(
|
|
|
173
199
|
responses,
|
|
174
200
|
outcomes,
|
|
175
201
|
templateValues,
|
|
202
|
+
interactionStates,
|
|
176
203
|
diagnostics,
|
|
177
204
|
);
|
|
178
205
|
return { outcomes: cloneValueRecord(outcomes), diagnostics, state };
|
|
@@ -184,6 +211,7 @@ export function createItemSession(
|
|
|
184
211
|
responses,
|
|
185
212
|
outcomes,
|
|
186
213
|
templateValues,
|
|
214
|
+
interactionStates,
|
|
187
215
|
validationMessages,
|
|
188
216
|
);
|
|
189
217
|
},
|
|
@@ -234,9 +262,19 @@ function assertCompatiblePriorState(
|
|
|
234
262
|
const templateIdentifiers = new Set(
|
|
235
263
|
document.item.templateDeclarations.map((declaration) => declaration.identifier),
|
|
236
264
|
);
|
|
265
|
+
const interactionStateIdentifiers = new Set(
|
|
266
|
+
document.item.interactions
|
|
267
|
+
.filter((interaction) => interaction.type === "portableCustom")
|
|
268
|
+
.map((interaction) => interaction.responseIdentifier)
|
|
269
|
+
.filter((identifier): identifier is string => Boolean(identifier)),
|
|
270
|
+
);
|
|
237
271
|
assertKnownStateIdentifiers("response", priorState.responses, responseIdentifiers);
|
|
238
272
|
assertKnownStateIdentifiers("outcome", priorState.outcomes, outcomeIdentifiers);
|
|
239
273
|
assertKnownStateIdentifiers("template", priorState.templateValues ?? {}, templateIdentifiers);
|
|
274
|
+
assertKnownPortableCustomStateIdentifiers(
|
|
275
|
+
priorState.interactionStates ?? {},
|
|
276
|
+
interactionStateIdentifiers,
|
|
277
|
+
);
|
|
240
278
|
for (const message of priorState.validationMessages) {
|
|
241
279
|
if (message.path && !responseIdentifiers.has(message.path)) {
|
|
242
280
|
throw new Error(`Cannot restore validation message for unknown response ${message.path}.`);
|
|
@@ -272,6 +310,14 @@ function assertKnownStateIdentifiers(
|
|
|
272
310
|
if (unknown) throw new Error(`Cannot restore unknown ${kind} identifier ${unknown}.`);
|
|
273
311
|
}
|
|
274
312
|
|
|
313
|
+
function assertKnownPortableCustomStateIdentifiers(
|
|
314
|
+
record: Record<string, QtiPortableCustomStateValue>,
|
|
315
|
+
allowed: Set<string>,
|
|
316
|
+
): void {
|
|
317
|
+
const unknown = Object.keys(record).find((identifier) => !allowed.has(identifier));
|
|
318
|
+
if (unknown) throw new Error(`Cannot restore unknown interaction state identifier ${unknown}.`);
|
|
319
|
+
}
|
|
320
|
+
|
|
275
321
|
function assertRestoredValueMatchesDeclaration(
|
|
276
322
|
kind: string,
|
|
277
323
|
declaration: QtiVariableDeclaration,
|
|
@@ -369,6 +415,12 @@ function attemptStateErrors(value: unknown): string[] {
|
|
|
369
415
|
if (value.templateValues !== undefined && !isQtiValueRecord(value.templateValues)) {
|
|
370
416
|
errors.push("QTI attempt state templateValues must be a record of QTI values.");
|
|
371
417
|
}
|
|
418
|
+
if (
|
|
419
|
+
value.interactionStates !== undefined &&
|
|
420
|
+
!isPortableCustomStateRecord(value.interactionStates)
|
|
421
|
+
) {
|
|
422
|
+
errors.push("QTI attempt state interactionStates must be a record of JSON values.");
|
|
423
|
+
}
|
|
372
424
|
if (!isDiagnosticArray(value.validationMessages)) {
|
|
373
425
|
errors.push("QTI attempt state validationMessages must be an array of diagnostics.");
|
|
374
426
|
}
|
|
@@ -1793,6 +1845,7 @@ function serialize(
|
|
|
1793
1845
|
responses: Record<string, QtiValue>,
|
|
1794
1846
|
outcomes: Record<string, QtiValue>,
|
|
1795
1847
|
templateValues: Record<string, QtiValue>,
|
|
1848
|
+
interactionStates: Record<string, QtiPortableCustomStateValue>,
|
|
1796
1849
|
validationMessages: QtiDiagnostic[],
|
|
1797
1850
|
): QtiAttemptStateV1 {
|
|
1798
1851
|
return {
|
|
@@ -1802,6 +1855,7 @@ function serialize(
|
|
|
1802
1855
|
responses: cloneValueRecord(responses),
|
|
1803
1856
|
outcomes: cloneValueRecord(outcomes),
|
|
1804
1857
|
templateValues: cloneValueRecord(templateValues),
|
|
1858
|
+
interactionStates: clonePortableCustomStateRecord(interactionStates),
|
|
1805
1859
|
validationMessages: cloneDiagnostics(validationMessages),
|
|
1806
1860
|
};
|
|
1807
1861
|
}
|
|
@@ -1810,12 +1864,26 @@ function cloneValueRecord(record: Record<string, QtiValue>): Record<string, QtiV
|
|
|
1810
1864
|
return Object.fromEntries(Object.entries(record).map(([key, value]) => [key, cloneValue(value)]));
|
|
1811
1865
|
}
|
|
1812
1866
|
|
|
1867
|
+
function clonePortableCustomStateRecord(
|
|
1868
|
+
record: Record<string, QtiPortableCustomStateValue>,
|
|
1869
|
+
): Record<string, QtiPortableCustomStateValue> {
|
|
1870
|
+
return Object.fromEntries(
|
|
1871
|
+
Object.entries(record).map(([key, value]) => [key, clonePortableCustomState(value)]),
|
|
1872
|
+
);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1813
1875
|
function cloneValue(value: QtiValue): QtiValue {
|
|
1814
1876
|
if (Array.isArray(value)) return [...value];
|
|
1815
1877
|
if (isRecordValue(value)) return cloneValueRecord(value);
|
|
1816
1878
|
return value;
|
|
1817
1879
|
}
|
|
1818
1880
|
|
|
1881
|
+
function clonePortableCustomState(value: QtiPortableCustomStateValue): QtiPortableCustomStateValue {
|
|
1882
|
+
if (Array.isArray(value)) return value.map(clonePortableCustomState);
|
|
1883
|
+
if (isPortableCustomStateObject(value)) return clonePortableCustomStateRecord(value);
|
|
1884
|
+
return value;
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1819
1887
|
function cloneDiagnostics(diagnostics: QtiDiagnostic[]): QtiDiagnostic[] {
|
|
1820
1888
|
return diagnostics.map((diagnostic) => ({
|
|
1821
1889
|
...diagnostic,
|
|
@@ -1937,6 +2005,28 @@ function isQtiValueRecord(value: unknown): value is Record<string, QtiValue> {
|
|
|
1937
2005
|
return Object.values(value).every(isQtiValue);
|
|
1938
2006
|
}
|
|
1939
2007
|
|
|
2008
|
+
function isPortableCustomState(value: unknown): value is QtiPortableCustomStateValue {
|
|
2009
|
+
if (value === null) return true;
|
|
2010
|
+
if (typeof value === "string" || typeof value === "boolean") return true;
|
|
2011
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
2012
|
+
if (Array.isArray(value)) return value.every(isPortableCustomState);
|
|
2013
|
+
if (isRecord(value)) return Object.values(value).every(isPortableCustomState);
|
|
2014
|
+
return false;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
function isPortableCustomStateObject(
|
|
2018
|
+
value: QtiPortableCustomStateValue,
|
|
2019
|
+
): value is { [key: string]: QtiPortableCustomStateValue } {
|
|
2020
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
function isPortableCustomStateRecord(
|
|
2024
|
+
value: unknown,
|
|
2025
|
+
): value is Record<string, QtiPortableCustomStateValue> {
|
|
2026
|
+
if (!isRecord(value)) return false;
|
|
2027
|
+
return Object.values(value).every(isPortableCustomState);
|
|
2028
|
+
}
|
|
2029
|
+
|
|
1940
2030
|
function isDiagnosticArray(value: unknown): value is QtiDiagnostic[] {
|
|
1941
2031
|
if (!Array.isArray(value)) return false;
|
|
1942
2032
|
return value.every((item) => {
|
package/src/support.ts
CHANGED
|
@@ -22,7 +22,7 @@ export const interactionSupport: QtiInteractionElementSupport[] = [
|
|
|
22
22
|
entry("qti-media-interaction", "media"),
|
|
23
23
|
entry("qti-order-interaction", "order"),
|
|
24
24
|
entry("qti-position-object-interaction", "positionObject"),
|
|
25
|
-
|
|
25
|
+
pciEntry(),
|
|
26
26
|
entry("qti-select-point-interaction", "selectPoint"),
|
|
27
27
|
entry("qti-slider-interaction", "slider"),
|
|
28
28
|
entry("qti-text-entry-interaction", "textEntry"),
|
|
@@ -225,6 +225,14 @@ function entry(qtiName: string, interactionType: QtiInteractionType): QtiInterac
|
|
|
225
225
|
};
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
function pciEntry(): QtiInteractionElementSupport {
|
|
229
|
+
return {
|
|
230
|
+
...entry("qti-portable-custom-interaction", "portableCustom"),
|
|
231
|
+
notes:
|
|
232
|
+
"Parses and validates PCI metadata, exposes a browser host contract, scores captured responses, and preserves opaque interaction state. Production module execution policy belongs to the host delivery runtime.",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
228
236
|
function processingEntry(
|
|
229
237
|
qtiName: string,
|
|
230
238
|
test: string,
|