@conform-ed/qti-react 0.0.12 → 0.0.13
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.js +4566 -212
- package/package.json +3 -1
- package/src/capability.ts +24 -0
- package/src/content-model.ts +104 -5
- package/src/graphic.ts +103 -0
- package/src/index.ts +139 -3
- package/src/interactions/associate.ts +22 -0
- package/src/interactions/drawing.ts +24 -0
- package/src/interactions/end-attempt.ts +19 -0
- package/src/interactions/extended-text.ts +21 -0
- package/src/interactions/gap-match.ts +22 -0
- package/src/interactions/graphic.ts +104 -0
- package/src/interactions/hottext.ts +21 -0
- package/src/interactions/index.ts +57 -2
- package/src/interactions/match.ts +27 -0
- package/src/interactions/media.ts +24 -0
- package/src/interactions/order.ts +21 -0
- package/src/interactions/slider.ts +24 -0
- package/src/interactions/upload.ts +19 -0
- package/src/normalized-item.ts +561 -0
- package/src/pci/index.ts +22 -0
- package/src/pci/interaction.ts +42 -0
- package/src/pci/markup.ts +102 -0
- package/src/pci/mount.ts +135 -0
- package/src/pci/registry.ts +240 -0
- package/src/pci/response.ts +138 -0
- package/src/pci/skin.ts +87 -0
- package/src/reference-skin/associate.ts +98 -0
- package/src/reference-skin/choice.ts +44 -0
- package/src/reference-skin/content.ts +30 -0
- package/src/reference-skin/drawing.ts +150 -0
- package/src/reference-skin/end-attempt.ts +27 -0
- package/src/reference-skin/extended-text.ts +35 -0
- package/src/reference-skin/gap-match.ts +69 -0
- package/src/reference-skin/graphic-associate.ts +123 -0
- package/src/reference-skin/graphic-base.ts +142 -0
- package/src/reference-skin/graphic-gap-match.ts +143 -0
- package/src/reference-skin/graphic-order.ts +76 -0
- package/src/reference-skin/hotspot.ts +43 -0
- package/src/reference-skin/hottext.ts +42 -0
- package/src/reference-skin/index.ts +75 -0
- package/src/reference-skin/inline-choice.ts +42 -0
- package/src/reference-skin/match.ts +80 -0
- package/src/reference-skin/media.ts +74 -0
- package/src/reference-skin/order.ts +79 -0
- package/src/reference-skin/position-object.ts +84 -0
- package/src/reference-skin/select-point.ts +87 -0
- package/src/reference-skin/slider.ts +41 -0
- package/src/reference-skin/text-entry.ts +31 -0
- package/src/reference-skin/upload.ts +46 -0
- package/src/response-processing.ts +178 -29
- package/src/rp/evaluate.ts +828 -0
- package/src/rp/index.ts +30 -0
- package/src/rp/interpreter.ts +251 -0
- package/src/rp/template-processing.ts +295 -0
- package/src/rp/templates.ts +190 -0
- package/src/rp/types.ts +161 -0
- package/src/rp/values.ts +198 -0
- package/src/runtime.ts +474 -28
- package/src/store.ts +155 -5
- package/src/test/controller.ts +806 -0
- package/src/test/index.ts +25 -0
- package/src/test/session-store.ts +244 -0
- package/src/test/types.ts +203 -0
- package/src/types.ts +27 -1
package/src/store.ts
CHANGED
|
@@ -1,19 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The response store the headless core owns (ADR-
|
|
2
|
+
* The response store the headless core owns (ADR-0001): it holds candidate responses
|
|
3
3
|
* keyed by `responseIdentifier`, the submitted flag, and the scored outcomes. Skins are
|
|
4
4
|
* controlled against it; they never own response state (only ephemeral UI state).
|
|
5
5
|
*
|
|
6
6
|
* Backed by an external store so `useSyncExternalStore` can subscribe with narrow,
|
|
7
7
|
* snapshot-stable reads. No React import here — the hook lives in the runtime.
|
|
8
|
+
*
|
|
9
|
+
* Template processing runs once at store creation under the given seed (ADR-0004):
|
|
10
|
+
* the seed is the replay key for a randomized clone. Adaptive items (`adaptive`)
|
|
11
|
+
* support multiple attempts: outcomes carry over between RP runs and the item locks
|
|
12
|
+
* only when `completionStatus` reaches "completed".
|
|
8
13
|
*/
|
|
9
14
|
|
|
10
15
|
import { scoreResponse } from "./response-processing";
|
|
16
|
+
import { applyCorrectResponseOverrides, executeResponseProcessing, executeTemplateProcessing, mulberry32 } from "./rp";
|
|
17
|
+
import type {
|
|
18
|
+
CustomOperatorImplementation,
|
|
19
|
+
OutcomeDeclarationView,
|
|
20
|
+
OutcomeValue,
|
|
21
|
+
ResponseNormalization,
|
|
22
|
+
ResponseProcessingView,
|
|
23
|
+
TemplateDeclarationView,
|
|
24
|
+
TemplateProcessingView,
|
|
25
|
+
} from "./rp";
|
|
11
26
|
import type { ResponseDeclarationView, ResponseValue, ScoreResult } from "./types";
|
|
12
27
|
|
|
13
28
|
export interface AttemptSnapshot {
|
|
14
29
|
readonly responses: Readonly<Record<string, ResponseValue>>;
|
|
15
30
|
readonly submitted: boolean;
|
|
31
|
+
/** Per-response heuristic results backing the per-interaction feedback chrome. */
|
|
16
32
|
readonly scores: readonly ScoreResult[];
|
|
33
|
+
/** Item outcomes of record from the RP interpreter; empty before submit or without RP. */
|
|
34
|
+
readonly outcomes: Readonly<Record<string, OutcomeValue>>;
|
|
35
|
+
/** This clone's template variables (empty without templateProcessing). */
|
|
36
|
+
readonly templateValues: Readonly<Record<string, OutcomeValue>>;
|
|
37
|
+
/** Completed attempts so far (only ever exceeds 1 for adaptive items). */
|
|
38
|
+
readonly attemptCount: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AttemptStoreOptions {
|
|
42
|
+
readonly outcomeDeclarations?: readonly OutcomeDeclarationView[];
|
|
43
|
+
readonly responseProcessing?: ResponseProcessingView;
|
|
44
|
+
/** The Response Normalization hook (ADR-0004); applies to scores and outcomes alike. */
|
|
45
|
+
readonly normalization?: ResponseNormalization;
|
|
46
|
+
readonly templateDeclarations?: readonly TemplateDeclarationView[];
|
|
47
|
+
readonly templateProcessing?: TemplateProcessingView;
|
|
48
|
+
/** Clone seed for template processing; store it to replay the same clone. */
|
|
49
|
+
readonly seed?: number;
|
|
50
|
+
/** QTI adaptive item: multiple attempts, outcome carry-over, completionStatus lock. */
|
|
51
|
+
readonly adaptive?: boolean;
|
|
52
|
+
/** Registered vendor `customOperator` implementations by class (opt-in). */
|
|
53
|
+
readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>>;
|
|
17
54
|
}
|
|
18
55
|
|
|
19
56
|
export interface AttemptStore {
|
|
@@ -22,6 +59,15 @@ export interface AttemptStore {
|
|
|
22
59
|
readonly getSnapshot: () => AttemptSnapshot;
|
|
23
60
|
readonly subscribe: (listener: () => void) => () => void;
|
|
24
61
|
readonly setResponse: (responseIdentifier: string, value: ResponseValue) => void;
|
|
62
|
+
/**
|
|
63
|
+
* Imperative interactions (PCI) hold their response internally; a collector pulls it
|
|
64
|
+
* at submit time, before scoring. Returning undefined leaves the response unchanged.
|
|
65
|
+
* Returns the unregister function.
|
|
66
|
+
*/
|
|
67
|
+
readonly registerResponseCollector: (
|
|
68
|
+
responseIdentifier: string,
|
|
69
|
+
collector: () => ResponseValue | undefined,
|
|
70
|
+
) => () => void;
|
|
25
71
|
readonly submit: () => readonly ScoreResult[];
|
|
26
72
|
readonly reset: () => void;
|
|
27
73
|
}
|
|
@@ -29,14 +75,35 @@ export interface AttemptStore {
|
|
|
29
75
|
export function createAttemptStore(
|
|
30
76
|
declarations: readonly ResponseDeclarationView[],
|
|
31
77
|
initialResponses: Readonly<Record<string, ResponseValue>>,
|
|
78
|
+
options?: AttemptStoreOptions,
|
|
32
79
|
): AttemptStore {
|
|
33
|
-
const
|
|
80
|
+
const seed = options?.seed ?? Math.floor(Math.random() * 2 ** 31);
|
|
81
|
+
const templateResult = options?.templateProcessing
|
|
82
|
+
? executeTemplateProcessing(options.templateProcessing, {
|
|
83
|
+
templateDeclarations: options.templateDeclarations ?? [],
|
|
84
|
+
responseDeclarations: declarations,
|
|
85
|
+
seed,
|
|
86
|
+
customOperators: options.customOperators,
|
|
87
|
+
})
|
|
88
|
+
: null;
|
|
89
|
+
// The clone's effective declarations: setCorrectResponse overrides applied.
|
|
90
|
+
const effectiveDeclarations = templateResult
|
|
91
|
+
? applyCorrectResponseOverrides(declarations, templateResult.correctResponseOverrides)
|
|
92
|
+
: declarations;
|
|
93
|
+
const declarationsById = new Map(effectiveDeclarations.map((declaration) => [declaration.identifier, declaration]));
|
|
34
94
|
const listeners = new Set<() => void>();
|
|
95
|
+
const responseCollectors = new Map<string, () => ResponseValue | undefined>();
|
|
96
|
+
// RP's random stream: seed-derived but independent of template processing's, and
|
|
97
|
+
// continuous across attempts — seed + submission sequence replays exact outcomes.
|
|
98
|
+
const rpRandom = mulberry32((seed ^ 0x9e3779b9) >>> 0);
|
|
35
99
|
|
|
36
100
|
let snapshot: AttemptSnapshot = {
|
|
37
101
|
responses: { ...initialResponses },
|
|
38
102
|
submitted: false,
|
|
39
103
|
scores: [],
|
|
104
|
+
outcomes: {},
|
|
105
|
+
templateValues: templateResult?.templateValues ?? {},
|
|
106
|
+
attemptCount: 0,
|
|
40
107
|
};
|
|
41
108
|
|
|
42
109
|
function emit(next: AttemptSnapshot): void {
|
|
@@ -49,10 +116,31 @@ export function createAttemptStore(
|
|
|
49
116
|
|
|
50
117
|
function computeScores(responses: Readonly<Record<string, ResponseValue>>): readonly ScoreResult[] {
|
|
51
118
|
return [...declarationsById.values()].map((declaration) =>
|
|
52
|
-
scoreResponse(declaration, responses[declaration.identifier] ?? null),
|
|
119
|
+
scoreResponse(declaration, responses[declaration.identifier] ?? null, options?.normalization),
|
|
53
120
|
);
|
|
54
121
|
}
|
|
55
122
|
|
|
123
|
+
function computeOutcomes(
|
|
124
|
+
responses: Readonly<Record<string, ResponseValue>>,
|
|
125
|
+
priorOutcomes?: Readonly<Record<string, OutcomeValue>>,
|
|
126
|
+
): Readonly<Record<string, OutcomeValue>> {
|
|
127
|
+
if (!options?.responseProcessing) {
|
|
128
|
+
return {};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return executeResponseProcessing(options.responseProcessing, {
|
|
132
|
+
responseDeclarations: effectiveDeclarations,
|
|
133
|
+
outcomeDeclarations: options.outcomeDeclarations ?? [],
|
|
134
|
+
responses,
|
|
135
|
+
normalization: options.normalization,
|
|
136
|
+
templateDeclarations: options.templateDeclarations,
|
|
137
|
+
templateValues: snapshot.templateValues,
|
|
138
|
+
priorOutcomes,
|
|
139
|
+
random: rpRandom,
|
|
140
|
+
customOperators: options.customOperators,
|
|
141
|
+
}).outcomes;
|
|
142
|
+
}
|
|
143
|
+
|
|
56
144
|
// Arrow-function properties: inherently bound, so they can be passed by reference
|
|
57
145
|
// (e.g. to useSyncExternalStore) without `this` hazards.
|
|
58
146
|
return {
|
|
@@ -77,16 +165,78 @@ export function createAttemptStore(
|
|
|
77
165
|
});
|
|
78
166
|
},
|
|
79
167
|
|
|
168
|
+
registerResponseCollector: (responseIdentifier, collector) => {
|
|
169
|
+
responseCollectors.set(responseIdentifier, collector);
|
|
170
|
+
|
|
171
|
+
return () => {
|
|
172
|
+
if (responseCollectors.get(responseIdentifier) === collector) {
|
|
173
|
+
responseCollectors.delete(responseIdentifier);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
},
|
|
177
|
+
|
|
80
178
|
submit: () => {
|
|
179
|
+
if (snapshot.submitted) {
|
|
180
|
+
return snapshot.scores;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Pull collector-held responses (PCI instances) before scoring.
|
|
184
|
+
let collected = snapshot.responses;
|
|
185
|
+
|
|
186
|
+
for (const [responseIdentifier, collector] of responseCollectors) {
|
|
187
|
+
const value = collector();
|
|
188
|
+
|
|
189
|
+
if (value !== undefined) {
|
|
190
|
+
collected = { ...collected, [responseIdentifier]: value };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (collected !== snapshot.responses) {
|
|
195
|
+
snapshot = { ...snapshot, responses: collected };
|
|
196
|
+
}
|
|
197
|
+
|
|
81
198
|
const scores = computeScores(snapshot.responses);
|
|
199
|
+
const priorOutcomes = options?.adaptive && snapshot.attemptCount > 0 ? snapshot.outcomes : undefined;
|
|
200
|
+
const outcomes = computeOutcomes(snapshot.responses, priorOutcomes);
|
|
201
|
+
// completion_status: the corpus's snake_case authoring of the same built-in.
|
|
202
|
+
const completionStatus = outcomes["completionStatus"] ?? outcomes["completion_status"];
|
|
203
|
+
const completed = !options?.adaptive || completionStatus === "completed";
|
|
204
|
+
|
|
205
|
+
let responses = snapshot.responses;
|
|
206
|
+
|
|
207
|
+
if (options?.adaptive && !completed) {
|
|
208
|
+
// Between adaptive attempts, endAttempt-style boolean responses reset (spec:
|
|
209
|
+
// endAttemptInteraction response variables are false at the start of an attempt).
|
|
210
|
+
responses = { ...responses };
|
|
211
|
+
|
|
212
|
+
for (const declaration of effectiveDeclarations) {
|
|
213
|
+
if (declaration.baseType === "boolean") {
|
|
214
|
+
responses = { ...responses, [declaration.identifier]: null };
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
82
218
|
|
|
83
|
-
emit({
|
|
219
|
+
emit({
|
|
220
|
+
...snapshot,
|
|
221
|
+
responses,
|
|
222
|
+
submitted: completed,
|
|
223
|
+
scores,
|
|
224
|
+
outcomes,
|
|
225
|
+
attemptCount: snapshot.attemptCount + 1,
|
|
226
|
+
});
|
|
84
227
|
|
|
85
228
|
return scores;
|
|
86
229
|
},
|
|
87
230
|
|
|
88
231
|
reset: () => {
|
|
89
|
-
emit({
|
|
232
|
+
emit({
|
|
233
|
+
responses: { ...initialResponses },
|
|
234
|
+
submitted: false,
|
|
235
|
+
scores: [],
|
|
236
|
+
outcomes: {},
|
|
237
|
+
templateValues: snapshot.templateValues,
|
|
238
|
+
attemptCount: 0,
|
|
239
|
+
});
|
|
90
240
|
},
|
|
91
241
|
};
|
|
92
242
|
}
|