@conform-ed/qti-react 0.0.15 → 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 +2486 -405
- 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/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
|
@@ -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;
|
package/dist/test/types.d.ts
CHANGED
|
@@ -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
|
|
33
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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.
|
|
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",
|
package/src/content-model.ts
CHANGED
|
@@ -39,7 +39,11 @@ export const v0InteractionKinds = [
|
|
|
39
39
|
|
|
40
40
|
export type V0InteractionKind = (typeof v0InteractionKinds)[number];
|
|
41
41
|
|
|
42
|
-
/**
|
|
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
|
-
|
|
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 {
|
|
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";
|