@conform-ed/qti-react 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/capability.d.ts +17 -0
  2. package/dist/content-model.d.ts +42 -0
  3. package/dist/graphic.d.ts +23 -0
  4. package/dist/index.d.ts +14 -0
  5. package/dist/index.js +4556 -212
  6. package/dist/interactions/associate.d.ts +2 -0
  7. package/dist/interactions/choice.d.ts +2 -0
  8. package/dist/interactions/drawing.d.ts +2 -0
  9. package/dist/interactions/end-attempt.d.ts +2 -0
  10. package/dist/interactions/extended-text.d.ts +2 -0
  11. package/dist/interactions/gap-match.d.ts +2 -0
  12. package/dist/interactions/graphic.d.ts +13 -0
  13. package/dist/interactions/hottext.d.ts +2 -0
  14. package/dist/interactions/index.d.ts +18 -0
  15. package/dist/interactions/inline-choice.d.ts +2 -0
  16. package/dist/interactions/match.d.ts +2 -0
  17. package/dist/interactions/media.d.ts +2 -0
  18. package/dist/interactions/order.d.ts +2 -0
  19. package/dist/interactions/slider.d.ts +2 -0
  20. package/dist/interactions/text-entry.d.ts +2 -0
  21. package/dist/interactions/upload.d.ts +2 -0
  22. package/dist/normalized-item.d.ts +30 -0
  23. package/dist/pci/index.d.ts +6 -0
  24. package/dist/pci/interaction.d.ts +8 -0
  25. package/dist/pci/markup.d.ts +10 -0
  26. package/dist/pci/mount.d.ts +50 -0
  27. package/dist/pci/registry.d.ts +53 -0
  28. package/dist/pci/response.d.ts +11 -0
  29. package/dist/pci/skin.d.ts +12 -0
  30. package/dist/reference-skin/associate.d.ts +8 -0
  31. package/dist/reference-skin/choice.d.ts +8 -0
  32. package/dist/reference-skin/content.d.ts +6 -0
  33. package/dist/reference-skin/drawing.d.ts +9 -0
  34. package/dist/reference-skin/end-attempt.d.ts +7 -0
  35. package/dist/reference-skin/extended-text.d.ts +6 -0
  36. package/dist/reference-skin/gap-match.d.ts +8 -0
  37. package/dist/reference-skin/graphic-associate.d.ts +8 -0
  38. package/dist/reference-skin/graphic-base.d.ts +39 -0
  39. package/dist/reference-skin/graphic-gap-match.d.ts +8 -0
  40. package/dist/reference-skin/graphic-order.d.ts +8 -0
  41. package/dist/reference-skin/hotspot.d.ts +8 -0
  42. package/dist/reference-skin/hottext.d.ts +8 -0
  43. package/dist/reference-skin/index.d.ts +30 -0
  44. package/dist/reference-skin/inline-choice.d.ts +7 -0
  45. package/dist/reference-skin/match.d.ts +8 -0
  46. package/dist/reference-skin/media.d.ts +9 -0
  47. package/dist/reference-skin/order.d.ts +8 -0
  48. package/dist/reference-skin/position-object.d.ts +9 -0
  49. package/dist/reference-skin/select-point.d.ts +8 -0
  50. package/dist/reference-skin/slider.d.ts +8 -0
  51. package/dist/reference-skin/text-entry.d.ts +6 -0
  52. package/dist/reference-skin/upload.d.ts +8 -0
  53. package/dist/response-processing.d.ts +48 -0
  54. package/dist/rp/evaluate.d.ts +35 -0
  55. package/dist/rp/index.d.ts +4 -0
  56. package/dist/rp/interpreter.d.ts +15 -0
  57. package/dist/rp/template-processing.d.ts +49 -0
  58. package/dist/rp/templates.d.ts +8 -0
  59. package/dist/rp/types.d.ts +158 -0
  60. package/dist/rp/values.d.ts +27 -0
  61. package/dist/runtime.d.ts +164 -0
  62. package/dist/store.d.ts +61 -0
  63. package/dist/test/controller.d.ts +11 -0
  64. package/dist/test/index.d.ts +3 -0
  65. package/dist/test/session-store.d.ts +46 -0
  66. package/dist/test/types.d.ts +194 -0
  67. package/dist/types.d.ts +58 -0
  68. package/package.json +8 -6
  69. package/src/capability.ts +24 -0
  70. package/src/content-model.ts +104 -5
  71. package/src/graphic.ts +103 -0
  72. package/src/index.ts +139 -3
  73. package/src/interactions/associate.ts +22 -0
  74. package/src/interactions/choice.ts +2 -2
  75. package/src/interactions/drawing.ts +24 -0
  76. package/src/interactions/end-attempt.ts +19 -0
  77. package/src/interactions/extended-text.ts +21 -0
  78. package/src/interactions/gap-match.ts +22 -0
  79. package/src/interactions/graphic.ts +104 -0
  80. package/src/interactions/hottext.ts +21 -0
  81. package/src/interactions/index.ts +57 -3
  82. package/src/interactions/inline-choice.ts +2 -2
  83. package/src/interactions/match.ts +27 -0
  84. package/src/interactions/media.ts +24 -0
  85. package/src/interactions/order.ts +21 -0
  86. package/src/interactions/slider.ts +24 -0
  87. package/src/interactions/text-entry.ts +2 -2
  88. package/src/interactions/upload.ts +19 -0
  89. package/src/normalized-item.ts +563 -0
  90. package/src/pci/index.ts +22 -0
  91. package/src/pci/interaction.ts +42 -0
  92. package/src/pci/markup.ts +102 -0
  93. package/src/pci/mount.ts +134 -0
  94. package/src/pci/registry.ts +240 -0
  95. package/src/pci/response.ts +138 -0
  96. package/src/pci/skin.ts +86 -0
  97. package/src/reference-skin/associate.ts +98 -0
  98. package/src/reference-skin/choice.ts +44 -0
  99. package/src/reference-skin/content.ts +30 -0
  100. package/src/reference-skin/drawing.ts +160 -0
  101. package/src/reference-skin/end-attempt.ts +27 -0
  102. package/src/reference-skin/extended-text.ts +35 -0
  103. package/src/reference-skin/gap-match.ts +69 -0
  104. package/src/reference-skin/graphic-associate.ts +123 -0
  105. package/src/reference-skin/graphic-base.ts +142 -0
  106. package/src/reference-skin/graphic-gap-match.ts +143 -0
  107. package/src/reference-skin/graphic-order.ts +76 -0
  108. package/src/reference-skin/hotspot.ts +43 -0
  109. package/src/reference-skin/hottext.ts +42 -0
  110. package/src/reference-skin/index.ts +74 -0
  111. package/src/reference-skin/inline-choice.ts +42 -0
  112. package/src/reference-skin/match.ts +80 -0
  113. package/src/reference-skin/media.ts +74 -0
  114. package/src/reference-skin/order.ts +79 -0
  115. package/src/reference-skin/position-object.ts +84 -0
  116. package/src/reference-skin/select-point.ts +87 -0
  117. package/src/reference-skin/slider.ts +41 -0
  118. package/src/reference-skin/text-entry.ts +31 -0
  119. package/src/reference-skin/upload.ts +46 -0
  120. package/src/response-processing.ts +178 -29
  121. package/src/rp/evaluate.ts +827 -0
  122. package/src/rp/index.ts +30 -0
  123. package/src/rp/interpreter.ts +254 -0
  124. package/src/rp/template-processing.ts +290 -0
  125. package/src/rp/templates.ts +190 -0
  126. package/src/rp/types.ts +167 -0
  127. package/src/rp/values.ts +211 -0
  128. package/src/runtime.ts +476 -28
  129. package/src/store.ts +161 -5
  130. package/src/test/controller.ts +809 -0
  131. package/src/test/index.ts +25 -0
  132. package/src/test/session-store.ts +243 -0
  133. package/src/test/types.ts +203 -0
  134. package/src/types.ts +27 -1
package/src/store.ts CHANGED
@@ -1,19 +1,62 @@
1
1
  /**
2
- * The response store the headless core owns (ADR-0002): it holds candidate responses
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
+ /**
42
+ * Consumer-supplied input. Optional members deliberately admit explicit
43
+ * `undefined` ("undefined means not provided"), so callers can pass
44
+ * maybe-undefined values straight through; the store reads every member
45
+ * field-wise (`??`/`?.`) and never spread-merges options over defaults.
46
+ */
47
+ export interface AttemptStoreOptions {
48
+ readonly outcomeDeclarations?: readonly OutcomeDeclarationView[] | undefined;
49
+ readonly responseProcessing?: ResponseProcessingView | undefined;
50
+ /** The Response Normalization hook (ADR-0004); applies to scores and outcomes alike. */
51
+ readonly normalization?: ResponseNormalization | undefined;
52
+ readonly templateDeclarations?: readonly TemplateDeclarationView[] | undefined;
53
+ readonly templateProcessing?: TemplateProcessingView | undefined;
54
+ /** Clone seed for template processing; store it to replay the same clone. */
55
+ readonly seed?: number | undefined;
56
+ /** QTI adaptive item: multiple attempts, outcome carry-over, completionStatus lock. */
57
+ readonly adaptive?: boolean | undefined;
58
+ /** Registered vendor `customOperator` implementations by class (opt-in). */
59
+ readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>> | undefined;
17
60
  }
18
61
 
19
62
  export interface AttemptStore {
@@ -22,6 +65,15 @@ export interface AttemptStore {
22
65
  readonly getSnapshot: () => AttemptSnapshot;
23
66
  readonly subscribe: (listener: () => void) => () => void;
24
67
  readonly setResponse: (responseIdentifier: string, value: ResponseValue) => void;
68
+ /**
69
+ * Imperative interactions (PCI) hold their response internally; a collector pulls it
70
+ * at submit time, before scoring. Returning undefined leaves the response unchanged.
71
+ * Returns the unregister function.
72
+ */
73
+ readonly registerResponseCollector: (
74
+ responseIdentifier: string,
75
+ collector: () => ResponseValue | undefined,
76
+ ) => () => void;
25
77
  readonly submit: () => readonly ScoreResult[];
26
78
  readonly reset: () => void;
27
79
  }
@@ -29,14 +81,35 @@ export interface AttemptStore {
29
81
  export function createAttemptStore(
30
82
  declarations: readonly ResponseDeclarationView[],
31
83
  initialResponses: Readonly<Record<string, ResponseValue>>,
84
+ options?: AttemptStoreOptions,
32
85
  ): AttemptStore {
33
- const declarationsById = new Map(declarations.map((declaration) => [declaration.identifier, declaration]));
86
+ const seed = options?.seed ?? Math.floor(Math.random() * 2 ** 31);
87
+ const templateResult = options?.templateProcessing
88
+ ? executeTemplateProcessing(options.templateProcessing, {
89
+ templateDeclarations: options.templateDeclarations ?? [],
90
+ responseDeclarations: declarations,
91
+ seed,
92
+ customOperators: options.customOperators,
93
+ })
94
+ : null;
95
+ // The clone's effective declarations: setCorrectResponse overrides applied.
96
+ const effectiveDeclarations = templateResult
97
+ ? applyCorrectResponseOverrides(declarations, templateResult.correctResponseOverrides)
98
+ : declarations;
99
+ const declarationsById = new Map(effectiveDeclarations.map((declaration) => [declaration.identifier, declaration]));
34
100
  const listeners = new Set<() => void>();
101
+ const responseCollectors = new Map<string, () => ResponseValue | undefined>();
102
+ // RP's random stream: seed-derived but independent of template processing's, and
103
+ // continuous across attempts — seed + submission sequence replays exact outcomes.
104
+ const rpRandom = mulberry32((seed ^ 0x9e3779b9) >>> 0);
35
105
 
36
106
  let snapshot: AttemptSnapshot = {
37
107
  responses: { ...initialResponses },
38
108
  submitted: false,
39
109
  scores: [],
110
+ outcomes: {},
111
+ templateValues: templateResult?.templateValues ?? {},
112
+ attemptCount: 0,
40
113
  };
41
114
 
42
115
  function emit(next: AttemptSnapshot): void {
@@ -49,10 +122,31 @@ export function createAttemptStore(
49
122
 
50
123
  function computeScores(responses: Readonly<Record<string, ResponseValue>>): readonly ScoreResult[] {
51
124
  return [...declarationsById.values()].map((declaration) =>
52
- scoreResponse(declaration, responses[declaration.identifier] ?? null),
125
+ scoreResponse(declaration, responses[declaration.identifier] ?? null, options?.normalization),
53
126
  );
54
127
  }
55
128
 
129
+ function computeOutcomes(
130
+ responses: Readonly<Record<string, ResponseValue>>,
131
+ priorOutcomes?: Readonly<Record<string, OutcomeValue>>,
132
+ ): Readonly<Record<string, OutcomeValue>> {
133
+ if (!options?.responseProcessing) {
134
+ return {};
135
+ }
136
+
137
+ return executeResponseProcessing(options.responseProcessing, {
138
+ responseDeclarations: effectiveDeclarations,
139
+ outcomeDeclarations: options.outcomeDeclarations ?? [],
140
+ responses,
141
+ normalization: options.normalization,
142
+ templateDeclarations: options.templateDeclarations,
143
+ templateValues: snapshot.templateValues,
144
+ priorOutcomes,
145
+ random: rpRandom,
146
+ customOperators: options.customOperators,
147
+ }).outcomes;
148
+ }
149
+
56
150
  // Arrow-function properties: inherently bound, so they can be passed by reference
57
151
  // (e.g. to useSyncExternalStore) without `this` hazards.
58
152
  return {
@@ -77,16 +171,78 @@ export function createAttemptStore(
77
171
  });
78
172
  },
79
173
 
174
+ registerResponseCollector: (responseIdentifier, collector) => {
175
+ responseCollectors.set(responseIdentifier, collector);
176
+
177
+ return () => {
178
+ if (responseCollectors.get(responseIdentifier) === collector) {
179
+ responseCollectors.delete(responseIdentifier);
180
+ }
181
+ };
182
+ },
183
+
80
184
  submit: () => {
185
+ if (snapshot.submitted) {
186
+ return snapshot.scores;
187
+ }
188
+
189
+ // Pull collector-held responses (PCI instances) before scoring.
190
+ let collected = snapshot.responses;
191
+
192
+ for (const [responseIdentifier, collector] of responseCollectors) {
193
+ const value = collector();
194
+
195
+ if (value !== undefined) {
196
+ collected = { ...collected, [responseIdentifier]: value };
197
+ }
198
+ }
199
+
200
+ if (collected !== snapshot.responses) {
201
+ snapshot = { ...snapshot, responses: collected };
202
+ }
203
+
81
204
  const scores = computeScores(snapshot.responses);
205
+ const priorOutcomes = options?.adaptive && snapshot.attemptCount > 0 ? snapshot.outcomes : undefined;
206
+ const outcomes = computeOutcomes(snapshot.responses, priorOutcomes);
207
+ // completion_status: the corpus's snake_case authoring of the same built-in.
208
+ const completionStatus = outcomes["completionStatus"] ?? outcomes["completion_status"];
209
+ const completed = !options?.adaptive || completionStatus === "completed";
210
+
211
+ let responses = snapshot.responses;
212
+
213
+ if (options?.adaptive && !completed) {
214
+ // Between adaptive attempts, endAttempt-style boolean responses reset (spec:
215
+ // endAttemptInteraction response variables are false at the start of an attempt).
216
+ responses = { ...responses };
217
+
218
+ for (const declaration of effectiveDeclarations) {
219
+ if (declaration.baseType === "boolean") {
220
+ responses = { ...responses, [declaration.identifier]: null };
221
+ }
222
+ }
223
+ }
82
224
 
83
- emit({ ...snapshot, submitted: true, scores });
225
+ emit({
226
+ ...snapshot,
227
+ responses,
228
+ submitted: completed,
229
+ scores,
230
+ outcomes,
231
+ attemptCount: snapshot.attemptCount + 1,
232
+ });
84
233
 
85
234
  return scores;
86
235
  },
87
236
 
88
237
  reset: () => {
89
- emit({ responses: { ...initialResponses }, submitted: false, scores: [] });
238
+ emit({
239
+ responses: { ...initialResponses },
240
+ submitted: false,
241
+ scores: [],
242
+ outcomes: {},
243
+ templateValues: snapshot.templateValues,
244
+ attemptCount: 0,
245
+ });
90
246
  },
91
247
  };
92
248
  }