@conform-ed/qti-react 0.0.15 → 0.0.17

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/src/test/types.ts CHANGED
@@ -8,6 +8,7 @@
8
8
  import type { CapabilityIssue } from "../capability";
9
9
  import type { OutcomeDeclarationView, OutcomeValue, RpExpressionView } from "../rp";
10
10
  import type { BodyNode } from "../runtime";
11
+ import type { ResponseValue } from "../types";
11
12
 
12
13
  export interface BranchRuleView {
13
14
  /** A target identifier in the same test part, or EXIT_TEST / EXIT_TESTPART / EXIT_SECTION. */
@@ -33,8 +34,10 @@ export interface ItemSessionControlView {
33
34
  }
34
35
 
35
36
  /**
36
- * QTI `timeLimits` (seconds). The controller is clock-free by design (ADR-0005): these
37
- * are data for the consumer's timers, which call `next()`/`end()` when time runs out.
37
+ * QTI `timeLimits` (seconds). The controller enforces these under an injectable clock
38
+ * (`TestControllerOptions.now`): expiry checks fold elapsed time at every transition,
39
+ * and consumers drive their own timers by calling `tick()` (ADR-0005, "Timing and
40
+ * time limits"). The values still surface on the plan for consumer-side countdowns.
38
41
  */
39
42
  export interface TimeLimitsView {
40
43
  readonly minTime?: number;
@@ -42,6 +45,16 @@ export interface TimeLimitsView {
42
45
  readonly allowLateSubmission?: boolean;
43
46
  }
44
47
 
48
+ /**
49
+ * "The default value of a template variable in an item can be overridden based on
50
+ * the test context in which the template is instantiated." (§5.152) The expression
51
+ * evaluates at test level (it may read other items' variables and test outcomes).
52
+ */
53
+ export interface TemplateDefaultView {
54
+ readonly templateIdentifier: string;
55
+ readonly expression: RpExpressionView;
56
+ }
57
+
45
58
  export interface AssessmentItemRefView {
46
59
  readonly kind: "assessmentItemRef";
47
60
  readonly identifier: string;
@@ -55,6 +68,7 @@ export interface AssessmentItemRefView {
55
68
  readonly timeLimits?: TimeLimitsView;
56
69
  /** Named weights for `testVariables`/aggregate weighting (missing names weigh 1). */
57
70
  readonly weights?: ReadonlyArray<{ readonly identifier: string; readonly value: number }>;
71
+ readonly templateDefaults?: readonly TemplateDefaultView[];
58
72
  }
59
73
 
60
74
  export interface AssessmentSectionView {
@@ -64,6 +78,12 @@ export interface AssessmentSectionView {
64
78
  readonly visible?: boolean;
65
79
  readonly fixed?: boolean;
66
80
  readonly required?: boolean;
81
+ /**
82
+ * For an invisible section under a shuffling parent: whether its children are
83
+ * "shuffled as a block or mixed up with the other children of the parent section"
84
+ * (§4.2.7). Default true (block).
85
+ */
86
+ readonly keepTogether?: boolean;
67
87
  readonly selection?: { readonly select: number; readonly withReplacement?: boolean };
68
88
  readonly ordering?: { readonly shuffle?: boolean };
69
89
  readonly preConditions?: readonly RpExpressionView[];
@@ -97,11 +117,16 @@ export interface OutcomeConditionBranch {
97
117
  readonly rules: readonly OutcomeRuleView[];
98
118
  }
99
119
 
100
- /** One outcome rule: outcomeCondition, setOutcomeValue, or exitTest. */
120
+ /**
121
+ * One outcome rule: outcomeCondition, setOutcomeValue, lookupOutcomeValue,
122
+ * outcomeProcessingFragment, or exitTest.
123
+ */
101
124
  export interface OutcomeRuleView {
102
125
  readonly kind: string;
103
126
  readonly identifier?: string;
104
127
  readonly expression?: RpExpressionView;
128
+ /** Nested rules of an `outcomeProcessingFragment` (§5.103). */
129
+ readonly rules?: readonly OutcomeRuleView[];
105
130
  readonly outcomeIf?: OutcomeConditionBranch;
106
131
  readonly outcomeElseIfs?: readonly OutcomeConditionBranch[];
107
132
  readonly outcomeElse?: { readonly rules: readonly OutcomeRuleView[] };
@@ -120,8 +145,17 @@ export interface AssessmentTestView {
120
145
  // ---------- The resolved delivery plan (selection + ordering applied) ----------
121
146
 
122
147
  export interface TestPlanItem {
123
- /** The item ref identifier — unique within the test, used as the session key. */
148
+ /**
149
+ * The session key: the ref identifier, or `identifier.n` when selection
150
+ * with-replacement instantiates the ref more than once — the spec's own instance
151
+ * addressing, where "a number that denotes the instance's place in the sequence of
152
+ * the item's instantiation is inserted between the item variable identifier and
153
+ * the item variable" (§2.11.1.2). Identifiers cannot contain periods, so the two
154
+ * forms never collide.
155
+ */
124
156
  readonly key: string;
157
+ /** 1-based instantiation number; present only when the ref has multiple instances. */
158
+ readonly instance?: number;
125
159
  readonly ref: AssessmentItemRefView;
126
160
  readonly partIdentifier: string;
127
161
  readonly sectionPath: readonly string[];
@@ -141,9 +175,17 @@ export interface TestPlanPart {
141
175
  readonly items: readonly TestPlanItem[];
142
176
  }
143
177
 
178
+ /** A section that survived selection, keyed for duration tracking and time limits. */
179
+ export interface TestPlanSection {
180
+ readonly identifier: string;
181
+ readonly timeLimits?: TimeLimitsView;
182
+ }
183
+
144
184
  export interface TestPlan {
145
185
  readonly timeLimits?: TimeLimitsView;
146
186
  readonly parts: readonly TestPlanPart[];
187
+ /** Every planned section by identifier (spec-unique across parts/sections/refs). */
188
+ readonly sections: Readonly<Record<string, TestPlanSection>>;
147
189
  }
148
190
 
149
191
  // ---------- Session state (consumer-persisted, plain JSON) ----------
@@ -159,10 +201,88 @@ export interface TestItemResult {
159
201
  readonly responded?: boolean;
160
202
  /** Adaptive items manage their own attempt lifecycle, so maxAttempts is ignored (spec). */
161
203
  readonly adaptive?: boolean;
204
+ /**
205
+ * The item session's elapsed seconds (`AttemptSnapshot.durationSeconds`). Resolves
206
+ * the built-in `ITEM.duration` in outcome processing; unreported → NULL.
207
+ */
208
+ readonly durationSeconds?: number;
209
+ /**
210
+ * Whether the responses satisfy the interaction constraints (response-validity).
211
+ * Under effective `validateResponses` in an individual-submission part, `false`
212
+ * makes the controller refuse the submission ("candidates are not allowed to
213
+ * submit the item until they have provided valid responses for all interactions").
214
+ */
215
+ readonly valid?: boolean;
216
+ /**
217
+ * The candidate's responses as submitted — recorded into the attempt history so
218
+ * results reporting can emit `candidateResponse` values from persisted state.
219
+ */
220
+ readonly responses?: Readonly<Record<string, ResponseValue>>;
221
+ /**
222
+ * The submission instant (epoch ms). Controller-stamped: callers never set it;
223
+ * it rides pending simultaneous results so the flush keeps submit-time stamps.
224
+ */
225
+ readonly submittedAtMs?: number;
226
+ }
227
+
228
+ /**
229
+ * One committed attempt, recorded for results reporting: "A report may contain
230
+ * multiple results for the same instance of an item representing multiple attempts
231
+ * … each item result must have a different datestamp."
232
+ */
233
+ export interface RecordedAttempt {
234
+ /** Submission instant (epoch ms) — the itemResult datestamp. */
235
+ readonly atMs: number;
236
+ readonly outcomes: Readonly<Record<string, OutcomeValue>>;
237
+ readonly responses?: Readonly<Record<string, ResponseValue>>;
238
+ /** The item session's elapsed seconds at this submission, when reported. */
239
+ readonly durationSeconds?: number;
240
+ }
241
+
242
+ /**
243
+ * Wall-clock accounting folded at every controller transition (and `tick()`).
244
+ * Durations are always "as of" `lastTransitionAtMs` — scoring and enforcement read
245
+ * recorded state only (ADR-0004 determinism). Until a suspend/resume API exists,
246
+ * these include all wall time between transitions.
247
+ */
248
+ export interface TestTimingState {
249
+ /** Injected-clock milliseconds at the last fold. */
250
+ readonly lastTransitionAtMs: number;
251
+ /** Whole-test seconds (the bare `duration` built-in, §2.8.5). */
252
+ readonly testSeconds: number;
253
+ /** Seconds per test-part identifier (`P1.duration`). */
254
+ readonly partSeconds: Readonly<Record<string, number>>;
255
+ /** Seconds per section identifier — a leaf accrues to every ancestor (`S2.duration`). */
256
+ readonly sectionSeconds: Readonly<Record<string, number>>;
257
+ /**
258
+ * Seconds each item has been the current item — the enforcement clock for item
259
+ * minTime/maxTime. The `ITEM.duration` variable reads the consumer report instead.
260
+ */
261
+ readonly itemSeconds: Readonly<Record<string, number>>;
262
+ }
263
+
264
+ export type TimingScopeRef =
265
+ | { readonly kind: "test" }
266
+ | { readonly kind: "part"; readonly identifier: string }
267
+ | { readonly kind: "section"; readonly identifier: string }
268
+ | { readonly kind: "item"; readonly key: string };
269
+
270
+ /** A late submission the controller refused (ADR-0003: no silent drops). */
271
+ export interface RejectedSubmission {
272
+ readonly itemKey: string;
273
+ /** The innermost exceeded scope whose allowLateSubmission did not permit it. */
274
+ readonly scope: TimingScopeRef;
275
+ /** Test-scope seconds at rejection (audit stamp on the session clock). */
276
+ readonly atTestSeconds: number;
162
277
  }
163
278
 
164
279
  export interface TestSessionState {
165
- readonly status: "in-progress" | "ended";
280
+ /**
281
+ * `suspended` stops every scope clock and blocks transitions until `resume()` —
282
+ * the gap never accrues to any duration ("minus any time the session was in the
283
+ * suspended state"). Pre-suspension persisted states only ever carry the other two.
284
+ */
285
+ readonly status: "in-progress" | "suspended" | "ended";
166
286
  readonly currentItemKey: string | null;
167
287
  readonly itemOutcomes: Readonly<Record<string, Readonly<Record<string, OutcomeValue>>>>;
168
288
  readonly attemptedItems: readonly string[];
@@ -181,9 +301,29 @@ export interface TestSessionState {
181
301
  */
182
302
  readonly pendingItemResults: Readonly<Record<string, TestItemResult>>;
183
303
  readonly testOutcomes: Readonly<Record<string, OutcomeValue>>;
304
+ /** Timing accumulators; absent on pre-timing persisted states (initialized lazily). */
305
+ readonly timing?: TestTimingState;
306
+ /**
307
+ * Evaluated `templateDefault` values per item key, recorded at the spec's times
308
+ * (§5.152: linear — when the item first becomes current; nonlinear — at testPart
309
+ * start) so item-store creation reads a stable, replayable value.
310
+ */
311
+ readonly templateDefaultValues?: Readonly<Record<string, Readonly<Record<string, OutcomeValue>>>>;
312
+ /** Latest consumer-reported item-session duration per item key (feeds `ITEM.duration`). */
313
+ readonly itemDurationSeconds?: Readonly<Record<string, number>>;
314
+ readonly rejectedSubmissions?: readonly RejectedSubmission[];
315
+ /**
316
+ * Candidate comments per item key (allowComment): "feedback from the candidate to
317
+ * the other actors in the assessment process", never part of the assessed responses.
318
+ */
319
+ readonly itemComments?: Readonly<Record<string, string>>;
320
+ /** Committed attempts per item key, in submission order (results reporting). */
321
+ readonly attemptHistory?: Readonly<Record<string, readonly RecordedAttempt[]>>;
184
322
  }
185
323
 
186
324
  export interface TestController {
325
+ /** The assessment test view this controller was created from. */
326
+ readonly test: AssessmentTestView;
187
327
  readonly plan: TestPlan;
188
328
  /** Static capability issues found in outcome processing, preconditions, and branch rules. */
189
329
  readonly issues: readonly CapabilityIssue[];
@@ -199,5 +339,32 @@ export interface TestController {
199
339
  readonly canSubmitItem: (state: TestSessionState, itemKey: string) => boolean;
200
340
  readonly submitItem: (state: TestSessionState, itemKey: string, result: TestItemResult) => TestSessionState;
201
341
  readonly end: (state: TestSessionState) => TestSessionState;
342
+ /**
343
+ * Fold elapsed time into the recorded durations and apply any max-time expiries
344
+ * (forced moves / forced end). Consumers run their own timers and call this;
345
+ * identity once the session has ended (the clock stops at end).
346
+ */
347
+ readonly tick: (state: TestSessionState) => TestSessionState;
202
348
  readonly visibleTestFeedbacks: (state: TestSessionState) => readonly TestFeedbackView[];
349
+ /**
350
+ * Post-end review (allowReview): whether the candidate may re-enter the item
351
+ * read-only — the session has ended, the item was presented, and its effective
352
+ * allowReview permits it.
353
+ */
354
+ readonly canReview: (state: TestSessionState, itemKey: string) => boolean;
355
+ /** Navigate review: sets the current item without reopening the ended session. */
356
+ readonly review: (state: TestSessionState, itemKey: string) => TestSessionState;
357
+ /** Whether a comment may be recorded: effective allowComment, session in progress. */
358
+ readonly canComment: (state: TestSessionState, itemKey: string) => boolean;
359
+ readonly setItemComment: (state: TestSessionState, itemKey: string, comment: string) => TestSessionState;
360
+ /**
361
+ * Suspend the session: folds the clock up to this instant (applying any expiry
362
+ * that fold reveals), then stops it. Identity unless in progress.
363
+ */
364
+ readonly suspend: (state: TestSessionState) => TestSessionState;
365
+ /**
366
+ * Resume a suspended session: re-stamps the clock at the current instant without
367
+ * folding the gap — suspended time never accrues. Identity unless suspended.
368
+ */
369
+ readonly resume: (state: TestSessionState) => TestSessionState;
203
370
  }
package/src/types.ts CHANGED
@@ -56,6 +56,7 @@ export interface ResponseDeclarationView {
56
56
  readonly identifier: string;
57
57
  readonly cardinality: Cardinality;
58
58
  readonly baseType?: string;
59
+ readonly defaultValue?: { readonly values: ReadonlyArray<{ readonly value: string | number | boolean }> };
59
60
  readonly correctResponse?: CorrectResponseView;
60
61
  readonly mapping?: MappingView;
61
62
  readonly areaMapping?: AreaMappingView;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Ambient typing for xspattern 3.1.0: the package ships dist/xspattern.d.ts and a
3
+ * top-level "types" field, but its "exports" map has no "types" condition, so
4
+ * modern resolution cannot see them. Mirrors the shipped declarations verbatim;
5
+ * delete once upstream adds the condition (https://github.com/bwrrp/xspattern.js).
6
+ */
7
+ declare module "xspattern" {
8
+ export type MatchFn = (str: string) => boolean;
9
+ export type Options = { language: "xsd" | "xpath" };
10
+ export function compile(pattern: string, options?: Options): MatchFn;
11
+ }