@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.
Files changed (42) hide show
  1. package/dist/index.d.ts +6 -4
  2. package/dist/index.js +2492 -408
  3. package/dist/normalized-item.d.ts +7 -5
  4. package/dist/pnp.d.ts +115 -0
  5. package/dist/response-validity.d.ts +28 -0
  6. package/dist/rp/evaluate.d.ts +6 -1
  7. package/dist/rp/index.d.ts +2 -2
  8. package/dist/rp/interpreter.d.ts +7 -1
  9. package/dist/rp/lookup-table.d.ts +17 -0
  10. package/dist/rp/template-processing.d.ts +5 -0
  11. package/dist/rp/types.d.ts +71 -7
  12. package/dist/runtime.d.ts +95 -0
  13. package/dist/store.d.ts +47 -0
  14. package/dist/test/controller.d.ts +22 -0
  15. package/dist/test/index.d.ts +2 -1
  16. package/dist/test/results.d.ts +102 -0
  17. package/dist/test/session-store.d.ts +32 -0
  18. package/dist/test/types.d.ts +173 -5
  19. package/dist/types.d.ts +5 -0
  20. package/package.json +5 -1
  21. package/src/content-model.ts +44 -4
  22. package/src/index.ts +43 -1
  23. package/src/normalized-item.ts +106 -4
  24. package/src/pci/mount.ts +11 -3
  25. package/src/pnp.ts +333 -0
  26. package/src/reference-skin/choice.ts +3 -0
  27. package/src/response-validity.ts +163 -0
  28. package/src/rp/evaluate.ts +280 -32
  29. package/src/rp/index.ts +5 -0
  30. package/src/rp/interpreter.ts +81 -1
  31. package/src/rp/lookup-table.ts +46 -0
  32. package/src/rp/template-processing.ts +41 -0
  33. package/src/rp/types.ts +75 -7
  34. package/src/runtime.ts +397 -20
  35. package/src/store.ts +146 -8
  36. package/src/test/controller.ts +856 -82
  37. package/src/test/index.ts +23 -0
  38. package/src/test/results.ts +378 -0
  39. package/src/test/session-store.ts +109 -1
  40. package/src/test/types.ts +172 -5
  41. package/src/types.ts +1 -0
  42. package/src/xspattern.d.ts +11 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * QTI Results Reporting — the export builder. Maps a session (test view + resolved
3
+ * plan + persisted state) onto the AssessmentResult model: one final itemResult per
4
+ * recorded attempt ("A report may contain multiple results for the same instance of
5
+ * an item representing multiple attempts … each item result must have a different
6
+ * datestamp"), pendingResponseProcessing for unflushed simultaneous submissions,
7
+ * and initial entries for everything else — "all items selected for presentation
8
+ * should be reported with a corresponding itemResult". The shapes mirror the
9
+ * contracts result schema structurally; serialization to XML lives in qti-xml.
10
+ */
11
+ import { type PnpView } from "../pnp";
12
+ import type { OutcomeDeclarationView } from "../rp";
13
+ import type { ResponseDeclarationView, ResponseValue } from "../types";
14
+ import type { AssessmentTestView, TestPlan, TestPlanItem, TestSessionState } from "./types";
15
+ export interface ResultValueView {
16
+ readonly value: string;
17
+ /** Required for record-cardinality members, invalid otherwise (schema rule). */
18
+ readonly fieldIdentifier?: string;
19
+ readonly baseType?: string;
20
+ }
21
+ export interface ResultResponseVariableView {
22
+ readonly identifier: string;
23
+ readonly cardinality: string;
24
+ readonly baseType?: string;
25
+ readonly candidateResponse: {
26
+ readonly values: readonly ResultValueView[];
27
+ };
28
+ readonly correctResponse?: {
29
+ readonly values: readonly ResultValueView[];
30
+ };
31
+ }
32
+ export interface ResultOutcomeVariableView {
33
+ readonly identifier: string;
34
+ readonly cardinality: string;
35
+ readonly baseType?: string;
36
+ readonly values: readonly ResultValueView[];
37
+ }
38
+ export type ResultSessionStatus = "final" | "initial" | "pendingExternalScoring" | "pendingResponseProcessing" | "pendingSubmission";
39
+ export interface ItemResultView {
40
+ readonly identifier: string;
41
+ readonly sequenceIndex?: number;
42
+ readonly datestamp: string;
43
+ readonly sessionStatus: ResultSessionStatus;
44
+ readonly responseVariables?: readonly ResultResponseVariableView[];
45
+ readonly outcomeVariables?: readonly ResultOutcomeVariableView[];
46
+ readonly candidateComment?: string;
47
+ }
48
+ /**
49
+ * RR Support (§2.6.4): a feature "aligned to the QTI profile of the 1EdTech Access
50
+ * for All Personal Needs and Preferences (AfA PNP)".
51
+ */
52
+ export interface ResultSupportView {
53
+ readonly name: string;
54
+ readonly assignment: "assigned" | "universal" | "prohibited" | "inherit";
55
+ readonly value?: string;
56
+ readonly xmlLang?: string;
57
+ }
58
+ export interface TestResultView {
59
+ readonly identifier: string;
60
+ readonly datestamp: string;
61
+ readonly responseVariables?: readonly ResultResponseVariableView[];
62
+ readonly outcomeVariables?: readonly ResultOutcomeVariableView[];
63
+ readonly supports?: readonly ResultSupportView[];
64
+ }
65
+ export interface ResultSessionIdentifierView {
66
+ readonly sourceId: string;
67
+ readonly identifier: string;
68
+ }
69
+ export interface ResultContextView {
70
+ readonly sourcedId?: string;
71
+ readonly sessionIdentifiers?: readonly ResultSessionIdentifierView[];
72
+ }
73
+ export interface AssessmentResultView {
74
+ readonly context: ResultContextView;
75
+ readonly testResult?: TestResultView;
76
+ readonly itemResults?: readonly ItemResultView[];
77
+ }
78
+ export interface AssessmentResultDocumentView {
79
+ readonly assessmentResult: AssessmentResultView;
80
+ }
81
+ export interface AssessmentResultItemDetails {
82
+ readonly responseDeclarations?: readonly ResponseDeclarationView[];
83
+ readonly outcomeDeclarations?: readonly OutcomeDeclarationView[];
84
+ /** The clone's resolved correct responses (`AttemptSnapshot.correctResponses`). */
85
+ readonly correctResponses?: Readonly<Record<string, ResponseValue>>;
86
+ }
87
+ export interface AssessmentResultInput {
88
+ readonly test: AssessmentTestView;
89
+ readonly plan: TestPlan;
90
+ readonly state: TestSessionState;
91
+ /** Result context (`sourcedId`, session identifiers); the consumer's identifiers. */
92
+ readonly context?: ResultContextView;
93
+ /** Export instant for the testResult datestamp (epoch ms; default Date.now()). */
94
+ readonly nowMs?: number;
95
+ /** Per-item typing and correct responses; omit (or return null) when unresolvable. */
96
+ readonly itemDetails?: (item: TestPlanItem) => AssessmentResultItemDetails | null;
97
+ /** The candidate's AfA PNP the session ran with — reported as testResult supports. */
98
+ readonly pnp?: PnpView | undefined;
99
+ }
100
+ export declare function buildAssessmentResult(input: AssessmentResultInput): AssessmentResultDocumentView;
101
+ /** Reshape a normalized assessmentResult document into the typed view (import side). */
102
+ export declare function assessmentResultFromNormalized(document: unknown): AssessmentResultView | null;
@@ -6,9 +6,11 @@
6
6
  * processing stays current. React-free — UI layers subscribe like any external store;
7
7
  * persistence stays with the consumer (store the seed and `snapshot.state`).
8
8
  */
9
+ import type { PnpView } from "../pnp";
9
10
  import type { CustomOperatorImplementation, ResponseNormalization } from "../rp";
10
11
  import type { AssessmentItemView } from "../runtime";
11
12
  import { type AttemptStore } from "../store";
13
+ import type { AssessmentResultDocumentView, ResultContextView } from "./results";
12
14
  import type { AssessmentItemRefView, TestController, TestFeedbackView, TestPlanItem, TestSessionState } from "./types";
13
15
  export interface TestSessionStoreOptions {
14
16
  /**
@@ -23,6 +25,16 @@ export interface TestSessionStoreOptions {
23
25
  readonly normalization?: ResponseNormalization;
24
26
  /** Registered vendor `customOperator` implementations by class (opt-in). */
25
27
  readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>>;
28
+ /**
29
+ * Millisecond clock for the item-session duration clocks (pair it with the
30
+ * controller's `now` for coherent timing). Defaults to Date.now.
31
+ */
32
+ readonly now?: () => number;
33
+ /**
34
+ * The candidate's AfA PNP — pair it with the controller's `pnp` (time-limit
35
+ * accommodations live there). The store reports it as result supports.
36
+ */
37
+ readonly pnp?: PnpView;
26
38
  }
27
39
  export interface TestSessionSnapshot {
28
40
  readonly state: TestSessionState;
@@ -42,5 +54,25 @@ export interface TestSessionStore {
42
54
  readonly canMoveTo: (itemKey: string) => boolean;
43
55
  readonly moveTo: (itemKey: string) => void;
44
56
  readonly end: () => void;
57
+ /** Fold elapsed time and apply time-limit expiries; consumers drive the cadence. */
58
+ readonly tick: () => void;
59
+ /** Post-end review navigation (allowReview); a no-op where review is barred. */
60
+ readonly review: (itemKey: string) => void;
61
+ /** Record a candidate comment (allowComment); a no-op where comments are barred. */
62
+ readonly setItemComment: (itemKey: string, comment: string) => void;
63
+ /** Suspend the session: every scope clock and the current item's clock stop. */
64
+ readonly suspend: () => void;
65
+ /** Resume a suspended session; the gap never accrues to any duration. */
66
+ readonly resume: () => void;
67
+ /**
68
+ * Build the QTI Results Reporting document for the session as it stands:
69
+ * per-attempt final itemResults, pending submissions, initial entries for
70
+ * everything else, typed by the resolved item views and the clones' correct
71
+ * responses. Serialize with qti-xml's `serializeQtiAssessmentResult`.
72
+ */
73
+ readonly assessmentResult: (options?: {
74
+ readonly context?: ResultContextView;
75
+ readonly nowMs?: number;
76
+ }) => AssessmentResultDocumentView;
45
77
  }
46
78
  export declare function createTestSessionStore(controller: TestController, options: TestSessionStoreOptions): TestSessionStore;
@@ -7,6 +7,7 @@
7
7
  import type { CapabilityIssue } from "../capability";
8
8
  import type { OutcomeDeclarationView, OutcomeValue, RpExpressionView } from "../rp";
9
9
  import type { BodyNode } from "../runtime";
10
+ import type { ResponseValue } from "../types";
10
11
  export interface BranchRuleView {
11
12
  /** A target identifier in the same test part, or EXIT_TEST / EXIT_TESTPART / EXIT_SECTION. */
12
13
  readonly target: string;
@@ -29,14 +30,25 @@ export interface ItemSessionControlView {
29
30
  readonly validateResponses?: boolean;
30
31
  }
31
32
  /**
32
- * QTI `timeLimits` (seconds). The controller is clock-free by design (ADR-0005): these
33
- * are data for the consumer's timers, which call `next()`/`end()` when time runs out.
33
+ * QTI `timeLimits` (seconds). The controller enforces these under an injectable clock
34
+ * (`TestControllerOptions.now`): expiry checks fold elapsed time at every transition,
35
+ * and consumers drive their own timers by calling `tick()` (ADR-0005, "Timing and
36
+ * time limits"). The values still surface on the plan for consumer-side countdowns.
34
37
  */
35
38
  export interface TimeLimitsView {
36
39
  readonly minTime?: number;
37
40
  readonly maxTime?: number;
38
41
  readonly allowLateSubmission?: boolean;
39
42
  }
43
+ /**
44
+ * "The default value of a template variable in an item can be overridden based on
45
+ * the test context in which the template is instantiated." (§5.152) The expression
46
+ * evaluates at test level (it may read other items' variables and test outcomes).
47
+ */
48
+ export interface TemplateDefaultView {
49
+ readonly templateIdentifier: string;
50
+ readonly expression: RpExpressionView;
51
+ }
40
52
  export interface AssessmentItemRefView {
41
53
  readonly kind: "assessmentItemRef";
42
54
  readonly identifier: string;
@@ -53,6 +65,7 @@ export interface AssessmentItemRefView {
53
65
  readonly identifier: string;
54
66
  readonly value: number;
55
67
  }>;
68
+ readonly templateDefaults?: readonly TemplateDefaultView[];
56
69
  }
57
70
  export interface AssessmentSectionView {
58
71
  readonly kind: "assessmentSection";
@@ -61,6 +74,12 @@ export interface AssessmentSectionView {
61
74
  readonly visible?: boolean;
62
75
  readonly fixed?: boolean;
63
76
  readonly required?: boolean;
77
+ /**
78
+ * For an invisible section under a shuffling parent: whether its children are
79
+ * "shuffled as a block or mixed up with the other children of the parent section"
80
+ * (§4.2.7). Default true (block).
81
+ */
82
+ readonly keepTogether?: boolean;
64
83
  readonly selection?: {
65
84
  readonly select: number;
66
85
  readonly withReplacement?: boolean;
@@ -95,11 +114,16 @@ export interface OutcomeConditionBranch {
95
114
  readonly expression: RpExpressionView;
96
115
  readonly rules: readonly OutcomeRuleView[];
97
116
  }
98
- /** One outcome rule: outcomeCondition, setOutcomeValue, or exitTest. */
117
+ /**
118
+ * One outcome rule: outcomeCondition, setOutcomeValue, lookupOutcomeValue,
119
+ * outcomeProcessingFragment, or exitTest.
120
+ */
99
121
  export interface OutcomeRuleView {
100
122
  readonly kind: string;
101
123
  readonly identifier?: string;
102
124
  readonly expression?: RpExpressionView;
125
+ /** Nested rules of an `outcomeProcessingFragment` (§5.103). */
126
+ readonly rules?: readonly OutcomeRuleView[];
103
127
  readonly outcomeIf?: OutcomeConditionBranch;
104
128
  readonly outcomeElseIfs?: readonly OutcomeConditionBranch[];
105
129
  readonly outcomeElse?: {
@@ -118,8 +142,17 @@ export interface AssessmentTestView {
118
142
  readonly testFeedbacks?: readonly TestFeedbackView[];
119
143
  }
120
144
  export interface TestPlanItem {
121
- /** The item ref identifier — unique within the test, used as the session key. */
145
+ /**
146
+ * The session key: the ref identifier, or `identifier.n` when selection
147
+ * with-replacement instantiates the ref more than once — the spec's own instance
148
+ * addressing, where "a number that denotes the instance's place in the sequence of
149
+ * the item's instantiation is inserted between the item variable identifier and
150
+ * the item variable" (§2.11.1.2). Identifiers cannot contain periods, so the two
151
+ * forms never collide.
152
+ */
122
153
  readonly key: string;
154
+ /** 1-based instantiation number; present only when the ref has multiple instances. */
155
+ readonly instance?: number;
123
156
  readonly ref: AssessmentItemRefView;
124
157
  readonly partIdentifier: string;
125
158
  readonly sectionPath: readonly string[];
@@ -137,9 +170,16 @@ export interface TestPlanPart {
137
170
  readonly timeLimits?: TimeLimitsView;
138
171
  readonly items: readonly TestPlanItem[];
139
172
  }
173
+ /** A section that survived selection, keyed for duration tracking and time limits. */
174
+ export interface TestPlanSection {
175
+ readonly identifier: string;
176
+ readonly timeLimits?: TimeLimitsView;
177
+ }
140
178
  export interface TestPlan {
141
179
  readonly timeLimits?: TimeLimitsView;
142
180
  readonly parts: readonly TestPlanPart[];
181
+ /** Every planned section by identifier (spec-unique across parts/sections/refs). */
182
+ readonly sections: Readonly<Record<string, TestPlanSection>>;
143
183
  }
144
184
  export interface TestItemResult {
145
185
  readonly outcomes: Readonly<Record<string, OutcomeValue>>;
@@ -152,9 +192,90 @@ export interface TestItemResult {
152
192
  readonly responded?: boolean;
153
193
  /** Adaptive items manage their own attempt lifecycle, so maxAttempts is ignored (spec). */
154
194
  readonly adaptive?: boolean;
195
+ /**
196
+ * The item session's elapsed seconds (`AttemptSnapshot.durationSeconds`). Resolves
197
+ * the built-in `ITEM.duration` in outcome processing; unreported → NULL.
198
+ */
199
+ readonly durationSeconds?: number;
200
+ /**
201
+ * Whether the responses satisfy the interaction constraints (response-validity).
202
+ * Under effective `validateResponses` in an individual-submission part, `false`
203
+ * makes the controller refuse the submission ("candidates are not allowed to
204
+ * submit the item until they have provided valid responses for all interactions").
205
+ */
206
+ readonly valid?: boolean;
207
+ /**
208
+ * The candidate's responses as submitted — recorded into the attempt history so
209
+ * results reporting can emit `candidateResponse` values from persisted state.
210
+ */
211
+ readonly responses?: Readonly<Record<string, ResponseValue>>;
212
+ /**
213
+ * The submission instant (epoch ms). Controller-stamped: callers never set it;
214
+ * it rides pending simultaneous results so the flush keeps submit-time stamps.
215
+ */
216
+ readonly submittedAtMs?: number;
217
+ }
218
+ /**
219
+ * One committed attempt, recorded for results reporting: "A report may contain
220
+ * multiple results for the same instance of an item representing multiple attempts
221
+ * … each item result must have a different datestamp."
222
+ */
223
+ export interface RecordedAttempt {
224
+ /** Submission instant (epoch ms) — the itemResult datestamp. */
225
+ readonly atMs: number;
226
+ readonly outcomes: Readonly<Record<string, OutcomeValue>>;
227
+ readonly responses?: Readonly<Record<string, ResponseValue>>;
228
+ /** The item session's elapsed seconds at this submission, when reported. */
229
+ readonly durationSeconds?: number;
230
+ }
231
+ /**
232
+ * Wall-clock accounting folded at every controller transition (and `tick()`).
233
+ * Durations are always "as of" `lastTransitionAtMs` — scoring and enforcement read
234
+ * recorded state only (ADR-0004 determinism). Until a suspend/resume API exists,
235
+ * these include all wall time between transitions.
236
+ */
237
+ export interface TestTimingState {
238
+ /** Injected-clock milliseconds at the last fold. */
239
+ readonly lastTransitionAtMs: number;
240
+ /** Whole-test seconds (the bare `duration` built-in, §2.8.5). */
241
+ readonly testSeconds: number;
242
+ /** Seconds per test-part identifier (`P1.duration`). */
243
+ readonly partSeconds: Readonly<Record<string, number>>;
244
+ /** Seconds per section identifier — a leaf accrues to every ancestor (`S2.duration`). */
245
+ readonly sectionSeconds: Readonly<Record<string, number>>;
246
+ /**
247
+ * Seconds each item has been the current item — the enforcement clock for item
248
+ * minTime/maxTime. The `ITEM.duration` variable reads the consumer report instead.
249
+ */
250
+ readonly itemSeconds: Readonly<Record<string, number>>;
251
+ }
252
+ export type TimingScopeRef = {
253
+ readonly kind: "test";
254
+ } | {
255
+ readonly kind: "part";
256
+ readonly identifier: string;
257
+ } | {
258
+ readonly kind: "section";
259
+ readonly identifier: string;
260
+ } | {
261
+ readonly kind: "item";
262
+ readonly key: string;
263
+ };
264
+ /** A late submission the controller refused (ADR-0003: no silent drops). */
265
+ export interface RejectedSubmission {
266
+ readonly itemKey: string;
267
+ /** The innermost exceeded scope whose allowLateSubmission did not permit it. */
268
+ readonly scope: TimingScopeRef;
269
+ /** Test-scope seconds at rejection (audit stamp on the session clock). */
270
+ readonly atTestSeconds: number;
155
271
  }
156
272
  export interface TestSessionState {
157
- readonly status: "in-progress" | "ended";
273
+ /**
274
+ * `suspended` stops every scope clock and blocks transitions until `resume()` —
275
+ * the gap never accrues to any duration ("minus any time the session was in the
276
+ * suspended state"). Pre-suspension persisted states only ever carry the other two.
277
+ */
278
+ readonly status: "in-progress" | "suspended" | "ended";
158
279
  readonly currentItemKey: string | null;
159
280
  readonly itemOutcomes: Readonly<Record<string, Readonly<Record<string, OutcomeValue>>>>;
160
281
  readonly attemptedItems: readonly string[];
@@ -173,8 +294,28 @@ export interface TestSessionState {
173
294
  */
174
295
  readonly pendingItemResults: Readonly<Record<string, TestItemResult>>;
175
296
  readonly testOutcomes: Readonly<Record<string, OutcomeValue>>;
297
+ /** Timing accumulators; absent on pre-timing persisted states (initialized lazily). */
298
+ readonly timing?: TestTimingState;
299
+ /**
300
+ * Evaluated `templateDefault` values per item key, recorded at the spec's times
301
+ * (§5.152: linear — when the item first becomes current; nonlinear — at testPart
302
+ * start) so item-store creation reads a stable, replayable value.
303
+ */
304
+ readonly templateDefaultValues?: Readonly<Record<string, Readonly<Record<string, OutcomeValue>>>>;
305
+ /** Latest consumer-reported item-session duration per item key (feeds `ITEM.duration`). */
306
+ readonly itemDurationSeconds?: Readonly<Record<string, number>>;
307
+ readonly rejectedSubmissions?: readonly RejectedSubmission[];
308
+ /**
309
+ * Candidate comments per item key (allowComment): "feedback from the candidate to
310
+ * the other actors in the assessment process", never part of the assessed responses.
311
+ */
312
+ readonly itemComments?: Readonly<Record<string, string>>;
313
+ /** Committed attempts per item key, in submission order (results reporting). */
314
+ readonly attemptHistory?: Readonly<Record<string, readonly RecordedAttempt[]>>;
176
315
  }
177
316
  export interface TestController {
317
+ /** The assessment test view this controller was created from. */
318
+ readonly test: AssessmentTestView;
178
319
  readonly plan: TestPlan;
179
320
  /** Static capability issues found in outcome processing, preconditions, and branch rules. */
180
321
  readonly issues: readonly CapabilityIssue[];
@@ -190,5 +331,32 @@ export interface TestController {
190
331
  readonly canSubmitItem: (state: TestSessionState, itemKey: string) => boolean;
191
332
  readonly submitItem: (state: TestSessionState, itemKey: string, result: TestItemResult) => TestSessionState;
192
333
  readonly end: (state: TestSessionState) => TestSessionState;
334
+ /**
335
+ * Fold elapsed time into the recorded durations and apply any max-time expiries
336
+ * (forced moves / forced end). Consumers run their own timers and call this;
337
+ * identity once the session has ended (the clock stops at end).
338
+ */
339
+ readonly tick: (state: TestSessionState) => TestSessionState;
193
340
  readonly visibleTestFeedbacks: (state: TestSessionState) => readonly TestFeedbackView[];
341
+ /**
342
+ * Post-end review (allowReview): whether the candidate may re-enter the item
343
+ * read-only — the session has ended, the item was presented, and its effective
344
+ * allowReview permits it.
345
+ */
346
+ readonly canReview: (state: TestSessionState, itemKey: string) => boolean;
347
+ /** Navigate review: sets the current item without reopening the ended session. */
348
+ readonly review: (state: TestSessionState, itemKey: string) => TestSessionState;
349
+ /** Whether a comment may be recorded: effective allowComment, session in progress. */
350
+ readonly canComment: (state: TestSessionState, itemKey: string) => boolean;
351
+ readonly setItemComment: (state: TestSessionState, itemKey: string, comment: string) => TestSessionState;
352
+ /**
353
+ * Suspend the session: folds the clock up to this instant (applying any expiry
354
+ * that fold reveals), then stops it. Identity unless in progress.
355
+ */
356
+ readonly suspend: (state: TestSessionState) => TestSessionState;
357
+ /**
358
+ * Resume a suspended session: re-stamps the clock at the current instant without
359
+ * folding the gap — suspended time never accrues. Identity unless suspended.
360
+ */
361
+ readonly resume: (state: TestSessionState) => TestSessionState;
194
362
  }
package/dist/types.d.ts CHANGED
@@ -45,6 +45,11 @@ export interface ResponseDeclarationView {
45
45
  readonly identifier: string;
46
46
  readonly cardinality: Cardinality;
47
47
  readonly baseType?: string;
48
+ readonly defaultValue?: {
49
+ readonly values: ReadonlyArray<{
50
+ readonly value: string | number | boolean;
51
+ }>;
52
+ };
48
53
  readonly correctResponse?: CorrectResponseView;
49
54
  readonly mapping?: MappingView;
50
55
  readonly areaMapping?: AreaMappingView;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conform-ed/qti-react",
3
- "version": "0.0.14",
3
+ "version": "0.0.16",
4
4
  "files": [
5
5
  "src",
6
6
  "dist"
@@ -20,7 +20,11 @@
20
20
  "test": "bun test",
21
21
  "typecheck": "tsgo --noEmit"
22
22
  },
23
+ "dependencies": {
24
+ "xspattern": "^3.1.0"
25
+ },
23
26
  "devDependencies": {
27
+ "@conform-ed/contracts": "0.0.16",
24
28
  "@conform-ed/qti-xml": "0.0.13",
25
29
  "@types/react": "^19.2.17",
26
30
  "@types/react-dom": "^19",
@@ -39,7 +39,11 @@ export const v0InteractionKinds = [
39
39
 
40
40
  export type V0InteractionKind = (typeof v0InteractionKinds)[number];
41
41
 
42
- /** Allowed HTML flow/inline element names for generic `kind: "xml"` body nodes. */
42
+ /**
43
+ * Allowed HTML flow/inline element names for generic `kind: "xml"` body nodes — the
44
+ * complete QTI HTML vocabulary as the official ASI XSD enumerates it
45
+ * (HTMLContentDType), not all of HTML5.
46
+ */
43
47
  const v0FlowElements = new Set<string>([
44
48
  "p",
45
49
  "span",
@@ -53,11 +57,14 @@ const v0FlowElements = new Set<string>([
53
57
  "ul",
54
58
  "ol",
55
59
  "li",
56
- // language-critical
60
+ // language-critical (§2.14: ruby/furigana, bidirectional text)
57
61
  "ruby",
58
62
  "rb",
59
63
  "rt",
60
64
  "rp",
65
+ "rtc",
66
+ "bdo",
67
+ "bdi",
61
68
  // media (the first media-milestone growth; src/poster route through the Asset Resolver)
62
69
  "img",
63
70
  "audio",
@@ -89,10 +96,38 @@ const v0FlowElements = new Set<string>([
89
96
  "tr",
90
97
  "th",
91
98
  "td",
99
+ // the rest of the XSD's enumerated vocabulary
100
+ "a",
101
+ "abbr",
102
+ "acronym",
103
+ "address",
104
+ "article",
105
+ "aside",
106
+ "big",
107
+ "code",
108
+ "details",
109
+ "summary",
110
+ "dfn",
111
+ "dl",
112
+ "dt",
113
+ "dd",
114
+ "kbd",
115
+ "label",
116
+ "nav",
117
+ "pre",
118
+ "q",
119
+ "samp",
120
+ "small",
121
+ "tt",
122
+ "var",
123
+ "footer",
124
+ "header",
92
125
  ]);
93
126
 
94
127
  /** Element-specific attribute allowlists, additive to the global set. */
95
128
  const v0ElementAttributes: ReadonlyMap<string, ReadonlySet<string>> = new Map([
129
+ // The XSD's attribute schematron for anchors: href and type beyond the global set.
130
+ ["a", new Set(["href", "type"])],
96
131
  ["img", new Set(["src", "alt", "width", "height"])],
97
132
  ["audio", new Set(["src", "controls", "loop", "muted", "preload"])],
98
133
  ["video", new Set(["src", "controls", "loop", "muted", "preload", "poster", "width", "height"])],
@@ -102,7 +137,7 @@ const v0ElementAttributes: ReadonlyMap<string, ReadonlySet<string>> = new Map([
102
137
  ]);
103
138
 
104
139
  /** Attribute names treated as packaged-asset references (rewritten by the Asset Resolver). */
105
- const v0UrlAttributes = new Set<string>(["src", "poster", "data"]);
140
+ const v0UrlAttributes = new Set<string>(["src", "poster", "data", "href"]);
106
141
 
107
142
  /**
108
143
  * The MathML root. Its subtree is rendered structurally (presentation MathML) with the
@@ -180,7 +215,12 @@ export function sanitizeAttributes(
180
215
  continue;
181
216
  }
182
217
 
183
- if (!model.globalAttributes.has(name) && !elementAllowed?.has(name)) {
218
+ // WAI-ARIA characteristics (§2.13.3) and data-* extension attributes are part of
219
+ // the QTI vocabulary (the XSD's own attribute schematron) and have no scripting
220
+ // surface; everything else must be allowlisted.
221
+ const ariaOrData = name === "role" || name.startsWith("aria-") || name.startsWith("data-");
222
+
223
+ if (!ariaOrData && !model.globalAttributes.has(name) && !elementAllowed?.has(name)) {
184
224
  continue;
185
225
  }
186
226
 
package/src/index.ts CHANGED
@@ -14,7 +14,11 @@ export {
14
14
 
15
15
  export { foldString, mapResponse, matchCorrect, mapResponsePoint, scoreResponse } from "./response-processing";
16
16
 
17
- export { assessmentItemViewFromNormalized, assessmentTestViewFromNormalized } from "./normalized-item";
17
+ export {
18
+ assessmentItemViewFromNormalized,
19
+ assessmentTestViewFromNormalized,
20
+ stimulusContentFromNormalized,
21
+ } from "./normalized-item";
18
22
 
19
23
  export { formatPoint, parseCoords, parsePoint, pointInShape, type Point, type QtiShape } from "./graphic";
20
24
 
@@ -30,6 +34,10 @@ export {
30
34
 
31
35
  export type {
32
36
  CustomOperatorImplementation,
37
+ InterpolationTableEntryView,
38
+ InterpolationTableView,
39
+ MatchTableEntryView,
40
+ MatchTableView,
33
41
  MaybeRpValue,
34
42
  OutcomeDeclarationView,
35
43
  OutcomeValue,
@@ -53,6 +61,14 @@ export type {
53
61
 
54
62
  export { createAttemptStore, type AttemptSnapshot, type AttemptStore, type AttemptStoreOptions } from "./store";
55
63
 
64
+ export {
65
+ collectInteractionConstraints,
66
+ collectResponseViolations,
67
+ type InteractionConstraint,
68
+ type ResponseConstraintKind,
69
+ type ResponseViolation,
70
+ } from "./response-validity";
71
+
56
72
  export {
57
73
  createTestController,
58
74
  createTestSessionStore,
@@ -69,18 +85,24 @@ export {
69
85
  type TestController,
70
86
  type TestFeedbackView,
71
87
  type TestItemResult,
88
+ type RejectedSubmission,
89
+ type TemplateDefaultView,
72
90
  type TestPartView,
73
91
  type TestPlan,
74
92
  type TestPlanItem,
75
93
  type TestPlanPart,
94
+ type TestPlanSection,
76
95
  type TestSessionState,
96
+ type TestTimingState,
77
97
  type TimeLimitsView,
98
+ type TimingScopeRef,
78
99
  } from "./test";
79
100
 
80
101
  export {
81
102
  createQtiRuntime,
82
103
  defineInteraction,
83
104
  type AssessmentItemView,
105
+ type AssessmentStimulusRefView,
84
106
  type AttemptController,
85
107
  type BodyNode,
86
108
  type CapabilityIssue,
@@ -94,12 +116,14 @@ export {
94
116
  type InteractionSkin,
95
117
  type InteractionStatus,
96
118
  type ItemRendererProps,
119
+ type ItemRenderMode,
97
120
  type NodeOverrides,
98
121
  type OptionProps,
99
122
  type OptionStatus,
100
123
  type QtiRuntime,
101
124
  type QtiRuntimeConfig,
102
125
  type SkinRegistry,
126
+ type StimulusContentView,
103
127
  type XmlContentNode,
104
128
  } from "./runtime";
105
129
 
@@ -183,3 +207,21 @@ export type {
183
207
  ResponseValue,
184
208
  ScoreResult,
185
209
  } from "./types";
210
+
211
+ export {
212
+ resolveCatalogSupports,
213
+ resolvePnpActivation,
214
+ type CatalogCardEntryView,
215
+ type CatalogCardView,
216
+ type CatalogContentView,
217
+ type CatalogFileHrefView,
218
+ type CatalogResolution,
219
+ type CatalogView,
220
+ type PnpActivation,
221
+ type PnpAdditionalTestingTimeView,
222
+ type PnpFeatureSetView,
223
+ type PnpLanguageModeView,
224
+ type PnpReplaceAccessModeView,
225
+ type PnpView,
226
+ type ResolvedCatalogSupport,
227
+ } from "./pnp";