@conform-ed/qti-react 0.0.14 → 0.0.16
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/dist/index.d.ts +6 -4
- package/dist/index.js +2492 -408
- package/dist/normalized-item.d.ts +7 -5
- package/dist/pnp.d.ts +115 -0
- package/dist/response-validity.d.ts +28 -0
- package/dist/rp/evaluate.d.ts +6 -1
- package/dist/rp/index.d.ts +2 -2
- package/dist/rp/interpreter.d.ts +7 -1
- package/dist/rp/lookup-table.d.ts +17 -0
- package/dist/rp/template-processing.d.ts +5 -0
- package/dist/rp/types.d.ts +71 -7
- package/dist/runtime.d.ts +95 -0
- package/dist/store.d.ts +47 -0
- package/dist/test/controller.d.ts +22 -0
- package/dist/test/index.d.ts +2 -1
- package/dist/test/results.d.ts +102 -0
- package/dist/test/session-store.d.ts +32 -0
- package/dist/test/types.d.ts +173 -5
- package/dist/types.d.ts +5 -0
- package/package.json +5 -1
- package/src/content-model.ts +44 -4
- package/src/index.ts +43 -1
- package/src/normalized-item.ts +106 -4
- package/src/pci/mount.ts +11 -3
- package/src/pnp.ts +333 -0
- package/src/reference-skin/choice.ts +3 -0
- package/src/response-validity.ts +163 -0
- package/src/rp/evaluate.ts +280 -32
- package/src/rp/index.ts +5 -0
- package/src/rp/interpreter.ts +81 -1
- package/src/rp/lookup-table.ts +46 -0
- package/src/rp/template-processing.ts +41 -0
- package/src/rp/types.ts +75 -7
- package/src/runtime.ts +397 -20
- package/src/store.ts +146 -8
- package/src/test/controller.ts +856 -82
- package/src/test/index.ts +23 -0
- package/src/test/results.ts +378 -0
- package/src/test/session-store.ts +109 -1
- package/src/test/types.ts +172 -5
- package/src/types.ts +1 -0
- package/src/xspattern.d.ts +11 -0
package/src/store.ts
CHANGED
|
@@ -13,7 +13,15 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { scoreResponse } from "./response-processing";
|
|
16
|
-
import {
|
|
16
|
+
import { collectResponseViolations } from "./response-validity";
|
|
17
|
+
import type { InteractionConstraint, ResponseViolation } from "./response-validity";
|
|
18
|
+
import {
|
|
19
|
+
applyCorrectResponseOverrides,
|
|
20
|
+
applyTemplateDefaultOverrides,
|
|
21
|
+
executeResponseProcessing,
|
|
22
|
+
executeTemplateProcessing,
|
|
23
|
+
mulberry32,
|
|
24
|
+
} from "./rp";
|
|
17
25
|
import type {
|
|
18
26
|
CustomOperatorImplementation,
|
|
19
27
|
OutcomeDeclarationView,
|
|
@@ -36,6 +44,23 @@ export interface AttemptSnapshot {
|
|
|
36
44
|
readonly templateValues: Readonly<Record<string, OutcomeValue>>;
|
|
37
45
|
/** Completed attempts so far (only ever exceeds 1 for adaptive items). */
|
|
38
46
|
readonly attemptCount: number;
|
|
47
|
+
/**
|
|
48
|
+
* Elapsed session seconds at the latest submit (the built-in `duration` response
|
|
49
|
+
* variable handed to RP); null before the first submit. Persist it alongside the
|
|
50
|
+
* responses for server-side replay parity (ADR-0004).
|
|
51
|
+
*/
|
|
52
|
+
readonly durationSeconds: number | null;
|
|
53
|
+
/**
|
|
54
|
+
* Interaction constraints the current responses fail (see response-validity).
|
|
55
|
+
* Always visible so UIs can explain themselves; submission is blocked on them
|
|
56
|
+
* only under `validateResponses`.
|
|
57
|
+
*/
|
|
58
|
+
readonly responseViolations: readonly ResponseViolation[];
|
|
59
|
+
/**
|
|
60
|
+
* This clone's resolved correct responses (template `setCorrectResponse` overrides
|
|
61
|
+
* applied), keyed by response identifier. The solution state renders these.
|
|
62
|
+
*/
|
|
63
|
+
readonly correctResponses: Readonly<Record<string, ResponseValue>>;
|
|
39
64
|
}
|
|
40
65
|
|
|
41
66
|
/**
|
|
@@ -51,12 +76,32 @@ export interface AttemptStoreOptions {
|
|
|
51
76
|
readonly normalization?: ResponseNormalization | undefined;
|
|
52
77
|
readonly templateDeclarations?: readonly TemplateDeclarationView[] | undefined;
|
|
53
78
|
readonly templateProcessing?: TemplateProcessingView | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Test-level `templateDefault` values (§5.152) overriding the template
|
|
81
|
+
* declarations' defaults for this clone; the test session store supplies them from
|
|
82
|
+
* the controller's recorded `templateDefaultValues`.
|
|
83
|
+
*/
|
|
84
|
+
readonly templateDefaultValues?: Readonly<Record<string, OutcomeValue>> | undefined;
|
|
54
85
|
/** Clone seed for template processing; store it to replay the same clone. */
|
|
55
86
|
readonly seed?: number | undefined;
|
|
56
87
|
/** QTI adaptive item: multiple attempts, outcome carry-over, completionStatus lock. */
|
|
57
88
|
readonly adaptive?: boolean | undefined;
|
|
58
89
|
/** Registered vendor `customOperator` implementations by class (opt-in). */
|
|
59
90
|
readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>> | undefined;
|
|
91
|
+
/**
|
|
92
|
+
* Millisecond clock backing the built-in `duration` response variable (wall-clock
|
|
93
|
+
* from session start to submit). Injectable for deterministic tests and replays;
|
|
94
|
+
* defaults to Date.now.
|
|
95
|
+
*/
|
|
96
|
+
readonly now?: (() => number) | undefined;
|
|
97
|
+
/** The item's interaction constraints (collectInteractionConstraints over the body). */
|
|
98
|
+
readonly constraints?: readonly InteractionConstraint[] | undefined;
|
|
99
|
+
/**
|
|
100
|
+
* ItemSessionControl validate-responses: "candidates are not allowed to submit the
|
|
101
|
+
* item until they have provided valid responses for all interactions". When set,
|
|
102
|
+
* submit() refuses while `responseViolations` is non-empty.
|
|
103
|
+
*/
|
|
104
|
+
readonly validateResponses?: boolean | undefined;
|
|
60
105
|
}
|
|
61
106
|
|
|
62
107
|
export interface AttemptStore {
|
|
@@ -76,6 +121,15 @@ export interface AttemptStore {
|
|
|
76
121
|
) => () => void;
|
|
77
122
|
readonly submit: () => readonly ScoreResult[];
|
|
78
123
|
readonly reset: () => void;
|
|
124
|
+
/**
|
|
125
|
+
* Stop the session clock: duration "records the accumulated time (in seconds) of
|
|
126
|
+
* all Candidate Sessions for all Attempts … minus any time the session was in the
|
|
127
|
+
* suspended state". Navigating away from an item suspends its session (spec);
|
|
128
|
+
* the test session store drives this. Idempotent.
|
|
129
|
+
*/
|
|
130
|
+
readonly suspend: () => void;
|
|
131
|
+
/** Restart the session clock after `suspend()`. Idempotent. */
|
|
132
|
+
readonly resume: () => void;
|
|
79
133
|
}
|
|
80
134
|
|
|
81
135
|
export function createAttemptStore(
|
|
@@ -84,9 +138,13 @@ export function createAttemptStore(
|
|
|
84
138
|
options?: AttemptStoreOptions,
|
|
85
139
|
): AttemptStore {
|
|
86
140
|
const seed = options?.seed ?? Math.floor(Math.random() * 2 ** 31);
|
|
141
|
+
// Test-level templateDefault values replace the declared defaults for this clone.
|
|
142
|
+
const templateDeclarations = options?.templateDefaultValues
|
|
143
|
+
? applyTemplateDefaultOverrides(options.templateDeclarations ?? [], options.templateDefaultValues)
|
|
144
|
+
: (options?.templateDeclarations ?? []);
|
|
87
145
|
const templateResult = options?.templateProcessing
|
|
88
146
|
? executeTemplateProcessing(options.templateProcessing, {
|
|
89
|
-
templateDeclarations
|
|
147
|
+
templateDeclarations,
|
|
90
148
|
responseDeclarations: declarations,
|
|
91
149
|
seed,
|
|
92
150
|
customOperators: options.customOperators,
|
|
@@ -102,14 +160,53 @@ export function createAttemptStore(
|
|
|
102
160
|
// RP's random stream: seed-derived but independent of template processing's, and
|
|
103
161
|
// continuous across attempts — seed + submission sequence replays exact outcomes.
|
|
104
162
|
const rpRandom = mulberry32((seed ^ 0x9e3779b9) >>> 0);
|
|
163
|
+
const now = options?.now ?? Date.now;
|
|
164
|
+
// The session clock: accumulated active milliseconds plus the running stretch.
|
|
165
|
+
// "the time between the beginning and the end of the item session minus any time
|
|
166
|
+
// the session was in the suspended state."
|
|
167
|
+
let activeMs = 0;
|
|
168
|
+
let runningSinceMs: number | null = now();
|
|
169
|
+
|
|
170
|
+
const activeSeconds = (): number => (activeMs + (runningSinceMs === null ? 0 : now() - runningSinceMs)) / 1000;
|
|
171
|
+
|
|
172
|
+
// The built-in completionStatus "starts with the reserved value 'not_attempted'.
|
|
173
|
+
// At the start of the first attempt it changes to the reserved value 'unknown'."
|
|
174
|
+
// (§2.2.2.3). The store exists only for a presented item, so its creation is the
|
|
175
|
+
// start of the first attempt; "not_attempted" is the state of items that never got
|
|
176
|
+
// a store. Explicit declarations (legacy content) keep the declared path.
|
|
177
|
+
const completionStatusDeclared = (options?.outcomeDeclarations ?? []).some(
|
|
178
|
+
(declaration) => declaration.identifier === "completionStatus",
|
|
179
|
+
);
|
|
180
|
+
const maintainedOutcomes = (): Readonly<Record<string, OutcomeValue>> =>
|
|
181
|
+
completionStatusDeclared ? {} : { completionStatus: "unknown" };
|
|
182
|
+
|
|
183
|
+
const violationsOf = (responses: Readonly<Record<string, ResponseValue>>): readonly ResponseViolation[] =>
|
|
184
|
+
options?.constraints ? collectResponseViolations(options.constraints, responses) : [];
|
|
185
|
+
|
|
186
|
+
// The clone's correct responses, flattened to response values for the solution state.
|
|
187
|
+
const correctResponses: Record<string, ResponseValue> = {};
|
|
188
|
+
|
|
189
|
+
for (const declaration of effectiveDeclarations) {
|
|
190
|
+
const values = declaration.correctResponse?.values;
|
|
191
|
+
|
|
192
|
+
if (values !== undefined) {
|
|
193
|
+
correctResponses[declaration.identifier] =
|
|
194
|
+
declaration.cardinality === "single"
|
|
195
|
+
? ((values[0]?.value ?? null) as ResponseValue)
|
|
196
|
+
: (values.map((entry) => entry.value) as never);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
105
199
|
|
|
106
200
|
let snapshot: AttemptSnapshot = {
|
|
107
201
|
responses: { ...initialResponses },
|
|
108
202
|
submitted: false,
|
|
109
203
|
scores: [],
|
|
110
|
-
outcomes:
|
|
204
|
+
outcomes: maintainedOutcomes(),
|
|
111
205
|
templateValues: templateResult?.templateValues ?? {},
|
|
112
206
|
attemptCount: 0,
|
|
207
|
+
durationSeconds: null,
|
|
208
|
+
responseViolations: violationsOf(initialResponses),
|
|
209
|
+
correctResponses,
|
|
113
210
|
};
|
|
114
211
|
|
|
115
212
|
function emit(next: AttemptSnapshot): void {
|
|
@@ -128,6 +225,7 @@ export function createAttemptStore(
|
|
|
128
225
|
|
|
129
226
|
function computeOutcomes(
|
|
130
227
|
responses: Readonly<Record<string, ResponseValue>>,
|
|
228
|
+
durationSeconds: number,
|
|
131
229
|
priorOutcomes?: Readonly<Record<string, OutcomeValue>>,
|
|
132
230
|
): Readonly<Record<string, OutcomeValue>> {
|
|
133
231
|
if (!options?.responseProcessing) {
|
|
@@ -139,11 +237,18 @@ export function createAttemptStore(
|
|
|
139
237
|
outcomeDeclarations: options.outcomeDeclarations ?? [],
|
|
140
238
|
responses,
|
|
141
239
|
normalization: options.normalization,
|
|
142
|
-
templateDeclarations
|
|
240
|
+
templateDeclarations,
|
|
143
241
|
templateValues: snapshot.templateValues,
|
|
144
242
|
priorOutcomes,
|
|
145
243
|
random: rpRandom,
|
|
146
244
|
customOperators: options.customOperators,
|
|
245
|
+
// Built-in session variables: numAttempts "increases by 1 at the start of
|
|
246
|
+
// each attempt", so the attempt being scored is included.
|
|
247
|
+
duration: durationSeconds,
|
|
248
|
+
numAttempts: snapshot.attemptCount + 1,
|
|
249
|
+
...(typeof snapshot.outcomes["completionStatus"] === "string"
|
|
250
|
+
? { completionStatus: snapshot.outcomes["completionStatus"] }
|
|
251
|
+
: {}),
|
|
147
252
|
}).outcomes;
|
|
148
253
|
}
|
|
149
254
|
|
|
@@ -165,9 +270,12 @@ export function createAttemptStore(
|
|
|
165
270
|
return;
|
|
166
271
|
}
|
|
167
272
|
|
|
273
|
+
const responses = { ...snapshot.responses, [responseIdentifier]: value };
|
|
274
|
+
|
|
168
275
|
emit({
|
|
169
276
|
...snapshot,
|
|
170
|
-
responses
|
|
277
|
+
responses,
|
|
278
|
+
responseViolations: violationsOf(responses),
|
|
171
279
|
});
|
|
172
280
|
},
|
|
173
281
|
|
|
@@ -198,12 +306,24 @@ export function createAttemptStore(
|
|
|
198
306
|
}
|
|
199
307
|
|
|
200
308
|
if (collected !== snapshot.responses) {
|
|
201
|
-
snapshot = { ...snapshot, responses: collected };
|
|
309
|
+
snapshot = { ...snapshot, responses: collected, responseViolations: violationsOf(collected) };
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// "candidates are not allowed to submit the item until they have provided
|
|
313
|
+
// valid responses for all interactions" — the refusal is visible through
|
|
314
|
+
// `responseViolations`, never silent (ADR-0003).
|
|
315
|
+
if (options?.validateResponses && snapshot.responseViolations.length > 0) {
|
|
316
|
+
emit(snapshot);
|
|
317
|
+
|
|
318
|
+
return snapshot.scores;
|
|
202
319
|
}
|
|
203
320
|
|
|
204
321
|
const scores = computeScores(snapshot.responses);
|
|
322
|
+
const durationSeconds = activeSeconds();
|
|
205
323
|
const priorOutcomes = options?.adaptive && snapshot.attemptCount > 0 ? snapshot.outcomes : undefined;
|
|
206
|
-
const
|
|
324
|
+
const rpOutcomes = computeOutcomes(snapshot.responses, durationSeconds, priorOutcomes);
|
|
325
|
+
// Items without responseProcessing still carry the maintained built-in.
|
|
326
|
+
const outcomes = options?.responseProcessing ? rpOutcomes : { ...maintainedOutcomes(), ...rpOutcomes };
|
|
207
327
|
// completion_status: the corpus's snake_case authoring of the same built-in.
|
|
208
328
|
const completionStatus = outcomes["completionStatus"] ?? outcomes["completion_status"];
|
|
209
329
|
const completed = !options?.adaptive || completionStatus === "completed";
|
|
@@ -229,19 +349,37 @@ export function createAttemptStore(
|
|
|
229
349
|
scores,
|
|
230
350
|
outcomes,
|
|
231
351
|
attemptCount: snapshot.attemptCount + 1,
|
|
352
|
+
durationSeconds,
|
|
232
353
|
});
|
|
233
354
|
|
|
234
355
|
return scores;
|
|
235
356
|
},
|
|
236
357
|
|
|
358
|
+
suspend: () => {
|
|
359
|
+
if (runningSinceMs !== null) {
|
|
360
|
+
activeMs += now() - runningSinceMs;
|
|
361
|
+
runningSinceMs = null;
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
resume: () => {
|
|
366
|
+
runningSinceMs ??= now();
|
|
367
|
+
},
|
|
368
|
+
|
|
237
369
|
reset: () => {
|
|
370
|
+
activeMs = 0;
|
|
371
|
+
runningSinceMs = now();
|
|
372
|
+
|
|
238
373
|
emit({
|
|
239
374
|
responses: { ...initialResponses },
|
|
240
375
|
submitted: false,
|
|
241
376
|
scores: [],
|
|
242
|
-
outcomes:
|
|
377
|
+
outcomes: maintainedOutcomes(),
|
|
243
378
|
templateValues: snapshot.templateValues,
|
|
244
379
|
attemptCount: 0,
|
|
380
|
+
durationSeconds: null,
|
|
381
|
+
responseViolations: violationsOf(initialResponses),
|
|
382
|
+
correctResponses,
|
|
245
383
|
});
|
|
246
384
|
},
|
|
247
385
|
};
|