@conform-ed/qti-react 0.0.18 → 0.0.20
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/headless.d.ts +20 -7
- package/dist/headless.js +1932 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +207 -0
- package/dist/item-score.d.ts +17 -0
- package/dist/rp/schema.d.ts +116 -0
- package/dist/test/schema.d.ts +192 -0
- package/package.json +4 -4
- package/src/headless.ts +87 -7
- package/src/index.ts +41 -0
- package/src/item-score.ts +40 -0
- package/src/rp/schema.ts +143 -0
- package/src/test/schema.ts +272 -0
package/src/headless.ts
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Headless (React-free) surface of @conform-ed/qti-react: the normalize → view adapters
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Headless (React-free) surface of @conform-ed/qti-react: the normalize → view adapters,
|
|
3
|
+
* the capability gate, and the **scoring engine** (Response Processing interpreter, the
|
|
4
|
+
* standard per-response scoring templates, the item-score aggregator, and the test-level
|
|
5
|
+
* outcome-processing controller). Importable on a server (e.g. a QTI ingest pipeline or an
|
|
6
|
+
* authoritative grade finalize) without pulling React. Exposed at
|
|
7
|
+
* `@conform-ed/qti-react/headless`; everything here is also re-exported from the package
|
|
8
|
+
* root for React consumers.
|
|
6
9
|
*
|
|
7
|
-
* Keep this entry free of React-coupled imports —
|
|
8
|
-
* ./item-capability
|
|
10
|
+
* Keep this entry free of React-coupled imports — every module re-exported here
|
|
11
|
+
* (./normalized-item, ./item-capability, ./response-processing, ./rp, ./item-score,
|
|
12
|
+
* ./store, ./test, ./response-validity) is verified framework-light.
|
|
9
13
|
*/
|
|
10
14
|
|
|
11
15
|
export {
|
|
@@ -14,6 +18,41 @@ export {
|
|
|
14
18
|
stimulusContentFromNormalized,
|
|
15
19
|
} from "./normalized-item";
|
|
16
20
|
export { referenceInteractionKinds, reportItemCapability, type ItemCapabilityOptions } from "./item-capability";
|
|
21
|
+
|
|
22
|
+
// The scoring engine, headless. Running these server-side re-derives the grade of record
|
|
23
|
+
// from the learner's raw responses + the (server-held) answer keys — see emergent ADR-0019.
|
|
24
|
+
export { foldString, mapResponse, matchCorrect, mapResponsePoint, scoreResponse } from "./response-processing";
|
|
25
|
+
export { effectiveItemScore, type EffectiveItemScore } from "./item-score";
|
|
26
|
+
export {
|
|
27
|
+
applyCorrectResponseOverrides,
|
|
28
|
+
collectRpIssues,
|
|
29
|
+
collectTemplateIssues,
|
|
30
|
+
executeResponseProcessing,
|
|
31
|
+
executeTemplateProcessing,
|
|
32
|
+
mulberry32,
|
|
33
|
+
resolveTemplate,
|
|
34
|
+
} from "./rp";
|
|
35
|
+
export type {
|
|
36
|
+
CustomOperatorImplementation,
|
|
37
|
+
InterpolationTableView,
|
|
38
|
+
MatchTableView,
|
|
39
|
+
MaybeRpValue,
|
|
40
|
+
OutcomeDeclarationView,
|
|
41
|
+
OutcomeValue,
|
|
42
|
+
ResponseNormalization,
|
|
43
|
+
ResponseProcessingContext,
|
|
44
|
+
ResponseProcessingResult,
|
|
45
|
+
ResponseProcessingView,
|
|
46
|
+
RpConditionBranch,
|
|
47
|
+
RpExpressionView,
|
|
48
|
+
RpRuleView,
|
|
49
|
+
RpScalar,
|
|
50
|
+
RpValue,
|
|
51
|
+
TemplateDeclarationView,
|
|
52
|
+
} from "./rp";
|
|
53
|
+
export { createAttemptStore, type AttemptSnapshot, type AttemptStore, type AttemptStoreOptions } from "./store";
|
|
54
|
+
export { createTestController, type TestController, type TestSessionState } from "./test";
|
|
55
|
+
|
|
17
56
|
export type { CapabilityIssue, CapabilityIssueType, CapabilityReport } from "./capability";
|
|
18
57
|
export type {
|
|
19
58
|
AssessmentItemView,
|
|
@@ -23,4 +62,45 @@ export type {
|
|
|
23
62
|
StimulusContentView,
|
|
24
63
|
XmlContentNode,
|
|
25
64
|
} from "./runtime";
|
|
26
|
-
export type { AssessmentTestView } from "./test";
|
|
65
|
+
export type { AssessmentItemRefView, AssessmentSectionView, AssessmentTestView, TestPartView } from "./test";
|
|
66
|
+
export type { Cardinality, ResponseDeclarationView, ResponseValue, ScoreResult } from "./types";
|
|
67
|
+
|
|
68
|
+
// QTI validation surface (zod mirrors of the views above) — atoms, the recursive
|
|
69
|
+
// rpExpression/outcome schemas, and the structure factory. React-free; reusable for
|
|
70
|
+
// import/parse validation and parameterized for authoring deltas (emergent's itemVersionId).
|
|
71
|
+
export {
|
|
72
|
+
cardinalitySchema,
|
|
73
|
+
interpolationTableSchema,
|
|
74
|
+
matchTableSchema,
|
|
75
|
+
outcomeDeclarationSchema,
|
|
76
|
+
rpExpressionSchema,
|
|
77
|
+
rpScalarSchema,
|
|
78
|
+
type OutcomeDeclarationSchema,
|
|
79
|
+
type RpExpressionSchema,
|
|
80
|
+
type RpScalarSchema,
|
|
81
|
+
} from "./rp/schema";
|
|
82
|
+
export {
|
|
83
|
+
assessmentItemRefViewSchema,
|
|
84
|
+
assessmentTestViewSchema,
|
|
85
|
+
branchRuleSchema,
|
|
86
|
+
itemSessionControlSchema,
|
|
87
|
+
makeAssessmentTestSchema,
|
|
88
|
+
orderingSchema,
|
|
89
|
+
outcomeConditionBranchSchema,
|
|
90
|
+
outcomeProcessingSchema,
|
|
91
|
+
outcomeRuleSchema,
|
|
92
|
+
selectionSchema,
|
|
93
|
+
testFeedbackSchema,
|
|
94
|
+
timeLimitsSchema,
|
|
95
|
+
type AssessmentItemRefViewSchema,
|
|
96
|
+
type AssessmentSectionNode,
|
|
97
|
+
type AssessmentTestNode,
|
|
98
|
+
type AssessmentTestSchemas,
|
|
99
|
+
type AssessmentTestViewSchema,
|
|
100
|
+
type BranchRuleSchema,
|
|
101
|
+
type ItemSessionControlSchema,
|
|
102
|
+
type OrderingSchema,
|
|
103
|
+
type SelectionSchema,
|
|
104
|
+
type TestPartNode,
|
|
105
|
+
type TimeLimitsSchema,
|
|
106
|
+
} from "./test/schema";
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ export {
|
|
|
14
14
|
|
|
15
15
|
export { foldString, mapResponse, matchCorrect, mapResponsePoint, scoreResponse } from "./response-processing";
|
|
16
16
|
|
|
17
|
+
export { effectiveItemScore, type EffectiveItemScore } from "./item-score";
|
|
18
|
+
|
|
17
19
|
export {
|
|
18
20
|
assessmentItemViewFromNormalized,
|
|
19
21
|
assessmentTestViewFromNormalized,
|
|
@@ -227,3 +229,42 @@ export {
|
|
|
227
229
|
type PnpView,
|
|
228
230
|
type ResolvedCatalogSupport,
|
|
229
231
|
} from "./pnp";
|
|
232
|
+
|
|
233
|
+
// QTI validation surface (zod mirrors of the assessmentTest/RP views). Also exposed at
|
|
234
|
+
// `@conform-ed/qti-react/headless` for server-side import/parse validation without React.
|
|
235
|
+
export {
|
|
236
|
+
cardinalitySchema,
|
|
237
|
+
interpolationTableSchema,
|
|
238
|
+
matchTableSchema,
|
|
239
|
+
outcomeDeclarationSchema,
|
|
240
|
+
rpExpressionSchema,
|
|
241
|
+
rpScalarSchema,
|
|
242
|
+
type OutcomeDeclarationSchema,
|
|
243
|
+
type RpExpressionSchema,
|
|
244
|
+
type RpScalarSchema,
|
|
245
|
+
} from "./rp/schema";
|
|
246
|
+
export {
|
|
247
|
+
assessmentItemRefViewSchema,
|
|
248
|
+
assessmentTestViewSchema,
|
|
249
|
+
branchRuleSchema,
|
|
250
|
+
itemSessionControlSchema,
|
|
251
|
+
makeAssessmentTestSchema,
|
|
252
|
+
orderingSchema,
|
|
253
|
+
outcomeConditionBranchSchema,
|
|
254
|
+
outcomeProcessingSchema,
|
|
255
|
+
outcomeRuleSchema,
|
|
256
|
+
selectionSchema,
|
|
257
|
+
testFeedbackSchema,
|
|
258
|
+
timeLimitsSchema,
|
|
259
|
+
type AssessmentItemRefViewSchema,
|
|
260
|
+
type AssessmentSectionNode,
|
|
261
|
+
type AssessmentTestNode,
|
|
262
|
+
type AssessmentTestSchemas,
|
|
263
|
+
type AssessmentTestViewSchema,
|
|
264
|
+
type BranchRuleSchema,
|
|
265
|
+
type ItemSessionControlSchema,
|
|
266
|
+
type OrderingSchema,
|
|
267
|
+
type SelectionSchema,
|
|
268
|
+
type TestPartNode,
|
|
269
|
+
type TimeLimitsSchema,
|
|
270
|
+
} from "./test/schema";
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ScoreResult } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface EffectiveItemScore {
|
|
4
|
+
readonly raw: number;
|
|
5
|
+
readonly max: number;
|
|
6
|
+
/** True when SCORE came from the RP outcomes of record rather than per-variable scoring. */
|
|
7
|
+
readonly fromOutcomes: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function numericOutcome(value: unknown): number | null {
|
|
11
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The item score of record (QTI): a numeric SCORE outcome from response processing is
|
|
16
|
+
* authoritative — PCI/RP-scored items (e.g. math-entry) have no per-variable
|
|
17
|
+
* correctResponse basis, so their standard scores read 0. Summed per-variable standard
|
|
18
|
+
* scoring is the fallback for items without RP. MAXSCORE follows the same precedence.
|
|
19
|
+
*
|
|
20
|
+
* Pure and framework-light: client and server (authoritative finalize) share it so the
|
|
21
|
+
* grade of record is derived identically on both sides.
|
|
22
|
+
*/
|
|
23
|
+
export function effectiveItemScore(
|
|
24
|
+
scores: readonly ScoreResult[],
|
|
25
|
+
outcomes: Readonly<Record<string, unknown>>,
|
|
26
|
+
): EffectiveItemScore {
|
|
27
|
+
const scoreOutcome = numericOutcome(outcomes["SCORE"]);
|
|
28
|
+
const maxOutcome = numericOutcome(outcomes["MAXSCORE"]);
|
|
29
|
+
const summedMax = scores.reduce((total, score) => total + score.maxScore, 0);
|
|
30
|
+
|
|
31
|
+
if (scoreOutcome !== null) {
|
|
32
|
+
return { raw: scoreOutcome, max: maxOutcome ?? summedMax, fromOutcomes: true };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
raw: scores.reduce((total, score) => total + score.score, 0),
|
|
37
|
+
max: maxOutcome ?? summedMax,
|
|
38
|
+
fromOutcomes: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
package/src/rp/schema.ts
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod mirrors of the Response-Processing view types (./types.ts). conform-ed owns the
|
|
3
|
+
* QTI view *types*; this is the validation surface that sits beside them — reusable for
|
|
4
|
+
* import/parse validation and kept honest against the types it mirrors. React-free.
|
|
5
|
+
*
|
|
6
|
+
* The expression union is deliberately permissive (`kind: z.string()`), exactly like
|
|
7
|
+
* `RpExpressionView`: kinds the interpreter does not implement still validate structurally
|
|
8
|
+
* and surface as `unsupported-rp` issues at evaluation time rather than being rejected here.
|
|
9
|
+
*
|
|
10
|
+
* The recursive schema is annotated against an explicit interface (`RpExpressionNode`); under
|
|
11
|
+
* `exactOptionalPropertyTypes` zod emits `T | undefined` optionals, so `z.infer` is a
|
|
12
|
+
* superset of the hand-written views (every view value parses — verified in the schema
|
|
13
|
+
* tests against real view samples). Callers that need the exact view shape map explicitly.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
|
|
18
|
+
import type { RpScalar } from "./types";
|
|
19
|
+
|
|
20
|
+
/** string | number | boolean — the QTI scalar value space (NULL is modelled by absence). */
|
|
21
|
+
export const rpScalarSchema = z.union([z.string(), z.number(), z.boolean()]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The inferred shape of `rpExpressionSchema` — a faithful, recursive mirror of
|
|
25
|
+
* `RpExpressionView` with zod's `T | undefined` optionals. Declared explicitly (and the
|
|
26
|
+
* schema is annotated with it) because zod's getter-based recursion degrades the recursive
|
|
27
|
+
* `expressions` field to `Record<string, unknown>` under `z.infer`; the `z.lazy` + interface
|
|
28
|
+
* form keeps the type precise (the expression-builder authoring relies on it).
|
|
29
|
+
*/
|
|
30
|
+
export interface RpExpressionNode {
|
|
31
|
+
readonly kind: string;
|
|
32
|
+
readonly identifier?: string | undefined;
|
|
33
|
+
readonly baseType?: string | undefined;
|
|
34
|
+
readonly value?: RpScalar | undefined;
|
|
35
|
+
readonly expressions?: readonly RpExpressionNode[] | undefined;
|
|
36
|
+
readonly min?: number | string | undefined;
|
|
37
|
+
readonly max?: number | string | undefined;
|
|
38
|
+
readonly step?: number | string | undefined;
|
|
39
|
+
readonly toleranceMode?: "exact" | "absolute" | "relative" | undefined;
|
|
40
|
+
readonly tolerance?: readonly (number | string)[] | undefined;
|
|
41
|
+
readonly includeLowerBound?: boolean | undefined;
|
|
42
|
+
readonly includeUpperBound?: boolean | undefined;
|
|
43
|
+
readonly n?: number | string | undefined;
|
|
44
|
+
readonly name?: string | undefined;
|
|
45
|
+
readonly roundingMode?: "decimalPlaces" | "significantFigures" | undefined;
|
|
46
|
+
readonly figures?: number | string | undefined;
|
|
47
|
+
readonly numberRepeats?: number | string | undefined;
|
|
48
|
+
readonly pattern?: string | undefined;
|
|
49
|
+
readonly caseSensitive?: boolean | undefined;
|
|
50
|
+
readonly substring?: boolean | undefined;
|
|
51
|
+
readonly shape?: string | undefined;
|
|
52
|
+
readonly coords?: string | undefined;
|
|
53
|
+
readonly variableIdentifier?: string | undefined;
|
|
54
|
+
readonly outcomeIdentifier?: string | undefined;
|
|
55
|
+
readonly weightIdentifier?: string | undefined;
|
|
56
|
+
readonly sectionIdentifier?: string | undefined;
|
|
57
|
+
readonly includeCategory?: string | readonly string[] | undefined;
|
|
58
|
+
readonly excludeCategory?: string | readonly string[] | undefined;
|
|
59
|
+
readonly class?: string | undefined;
|
|
60
|
+
readonly definition?: string | undefined;
|
|
61
|
+
readonly fieldIdentifier?: string | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A numeric attribute that may also be a variable reference (§2.11.3.6 / §7.13): the
|
|
66
|
+
* random/aggregate bounds, `index`/`anyN` positions, rounding `figures`, `repeat` counts.
|
|
67
|
+
*/
|
|
68
|
+
const numberOrRef = z.union([z.number(), z.string()]);
|
|
69
|
+
|
|
70
|
+
/** `includeCategory` / `excludeCategory` accept a single category or a list. */
|
|
71
|
+
const categoryFilter = z.union([z.string(), z.array(z.string())]);
|
|
72
|
+
|
|
73
|
+
export const cardinalitySchema = z.enum(["single", "multiple", "ordered", "record"]);
|
|
74
|
+
|
|
75
|
+
/** The recursive expression schema (`z.lazy` recursion; annotated for a precise `z.infer`). */
|
|
76
|
+
export const rpExpressionSchema: z.ZodType<RpExpressionNode> = z.lazy(() =>
|
|
77
|
+
z.object({
|
|
78
|
+
kind: z.string(),
|
|
79
|
+
identifier: z.string().optional(),
|
|
80
|
+
baseType: z.string().optional(),
|
|
81
|
+
value: rpScalarSchema.optional(),
|
|
82
|
+
expressions: z.array(rpExpressionSchema).optional(),
|
|
83
|
+
min: numberOrRef.optional(),
|
|
84
|
+
max: numberOrRef.optional(),
|
|
85
|
+
step: numberOrRef.optional(),
|
|
86
|
+
toleranceMode: z.enum(["exact", "absolute", "relative"]).optional(),
|
|
87
|
+
tolerance: z.array(numberOrRef).optional(),
|
|
88
|
+
includeLowerBound: z.boolean().optional(),
|
|
89
|
+
includeUpperBound: z.boolean().optional(),
|
|
90
|
+
n: numberOrRef.optional(),
|
|
91
|
+
name: z.string().optional(),
|
|
92
|
+
roundingMode: z.enum(["decimalPlaces", "significantFigures"]).optional(),
|
|
93
|
+
figures: numberOrRef.optional(),
|
|
94
|
+
numberRepeats: numberOrRef.optional(),
|
|
95
|
+
pattern: z.string().optional(),
|
|
96
|
+
caseSensitive: z.boolean().optional(),
|
|
97
|
+
substring: z.boolean().optional(),
|
|
98
|
+
shape: z.string().optional(),
|
|
99
|
+
coords: z.string().optional(),
|
|
100
|
+
variableIdentifier: z.string().optional(),
|
|
101
|
+
outcomeIdentifier: z.string().optional(),
|
|
102
|
+
weightIdentifier: z.string().optional(),
|
|
103
|
+
sectionIdentifier: z.string().optional(),
|
|
104
|
+
includeCategory: categoryFilter.optional(),
|
|
105
|
+
excludeCategory: categoryFilter.optional(),
|
|
106
|
+
class: z.string().optional(),
|
|
107
|
+
definition: z.string().optional(),
|
|
108
|
+
fieldIdentifier: z.string().optional(),
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// ---------- Outcome declarations (lookup tables) ----------
|
|
113
|
+
|
|
114
|
+
export const matchTableSchema = z.object({
|
|
115
|
+
defaultValue: rpScalarSchema.optional(),
|
|
116
|
+
matchTableEntries: z.array(z.object({ sourceValue: z.number(), targetValue: rpScalarSchema })),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
export const interpolationTableSchema = z.object({
|
|
120
|
+
defaultValue: rpScalarSchema.optional(),
|
|
121
|
+
interpolationTableEntries: z.array(
|
|
122
|
+
z.object({
|
|
123
|
+
sourceValue: z.number(),
|
|
124
|
+
targetValue: rpScalarSchema,
|
|
125
|
+
includeBoundary: z.boolean().optional(),
|
|
126
|
+
}),
|
|
127
|
+
),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export const outcomeDeclarationSchema = z.object({
|
|
131
|
+
identifier: z.string(),
|
|
132
|
+
cardinality: cardinalitySchema,
|
|
133
|
+
baseType: z.string().optional(),
|
|
134
|
+
defaultValue: z.object({ values: z.array(z.object({ value: rpScalarSchema })) }).optional(),
|
|
135
|
+
matchTable: matchTableSchema.optional(),
|
|
136
|
+
interpolationTable: interpolationTableSchema.optional(),
|
|
137
|
+
normalMaximum: z.number().optional(),
|
|
138
|
+
normalMinimum: z.number().optional(),
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export type RpScalarSchema = z.infer<typeof rpScalarSchema>;
|
|
142
|
+
export type RpExpressionSchema = z.infer<typeof rpExpressionSchema>;
|
|
143
|
+
export type OutcomeDeclarationSchema = z.infer<typeof outcomeDeclarationSchema>;
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod mirrors of the `assessmentTest` structural views (./types.ts) — the shared QTI
|
|
3
|
+
* atoms (`timeLimits`, `itemSessionControl`, `selection`, `ordering`, `branchRule`,
|
|
4
|
+
* test-level outcome processing/feedback) and the recursive structure
|
|
5
|
+
* (`assessmentSection` / `testPart` / `assessmentTest`).
|
|
6
|
+
*
|
|
7
|
+
* The structure is exposed as a **factory parameterized by the itemRef schema** —
|
|
8
|
+
* `makeAssessmentTestSchema(itemRefSchema)` — because the only thing that varies between
|
|
9
|
+
* delivery (an `href`) and authoring (an `itemVersionId`) is the item reference's identity.
|
|
10
|
+
* The ready `assessmentTestViewSchema` binds the delivery `href` itemRef; emergent binds
|
|
11
|
+
* its authoring itemRef. React-free; see ../rp/schema.ts for the RP-level mirrors.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
outcomeDeclarationSchema,
|
|
18
|
+
type OutcomeDeclarationSchema,
|
|
19
|
+
type RpExpressionSchema,
|
|
20
|
+
rpExpressionSchema,
|
|
21
|
+
} from "../rp/schema";
|
|
22
|
+
|
|
23
|
+
// ---------- Shared atoms (cascade levels: testPart → section → itemRef) ----------
|
|
24
|
+
|
|
25
|
+
/** `timeLimits` (seconds); the controller enforces these under its injected clock. */
|
|
26
|
+
export const timeLimitsSchema = z.object({
|
|
27
|
+
minTime: z.number().nonnegative().optional(),
|
|
28
|
+
maxTime: z.number().nonnegative().optional(),
|
|
29
|
+
allowLateSubmission: z.boolean().optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/** `itemSessionControl`: per-level overrides cascading testPart → section → itemRef. */
|
|
33
|
+
export const itemSessionControlSchema = z.object({
|
|
34
|
+
maxAttempts: z.number().int().nonnegative().optional(),
|
|
35
|
+
showFeedback: z.boolean().optional(),
|
|
36
|
+
allowReview: z.boolean().optional(),
|
|
37
|
+
showSolution: z.boolean().optional(),
|
|
38
|
+
allowComment: z.boolean().optional(),
|
|
39
|
+
allowSkipping: z.boolean().optional(),
|
|
40
|
+
validateResponses: z.boolean().optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/** `selection`: how many children to draw, optionally with replacement (§4.2.6). */
|
|
44
|
+
export const selectionSchema = z.object({
|
|
45
|
+
select: z.number().int().nonnegative(),
|
|
46
|
+
withReplacement: z.boolean().optional(),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** `ordering`: whether the surviving children are shuffled (§4.2.7). */
|
|
50
|
+
export const orderingSchema = z.object({ shuffle: z.boolean().optional() });
|
|
51
|
+
|
|
52
|
+
export type TimeLimitsSchema = z.infer<typeof timeLimitsSchema>;
|
|
53
|
+
export type ItemSessionControlSchema = z.infer<typeof itemSessionControlSchema>;
|
|
54
|
+
export type SelectionSchema = z.infer<typeof selectionSchema>;
|
|
55
|
+
export type OrderingSchema = z.infer<typeof orderingSchema>;
|
|
56
|
+
|
|
57
|
+
/** `branchRule`: a target identifier (or EXIT_*) gated by a boolean expression. */
|
|
58
|
+
export const branchRuleSchema = z.object({
|
|
59
|
+
target: z.string().trim().min(1),
|
|
60
|
+
expression: rpExpressionSchema,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
export type BranchRuleSchema = z.infer<typeof branchRuleSchema>;
|
|
64
|
+
|
|
65
|
+
const weightSchema = z.object({ identifier: z.string().trim().min(1), value: z.number() });
|
|
66
|
+
|
|
67
|
+
const templateDefaultSchema = z.object({
|
|
68
|
+
templateIdentifier: z.string().trim().min(1),
|
|
69
|
+
expression: rpExpressionSchema,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// ---------- Test-level outcome processing + feedback ----------
|
|
73
|
+
|
|
74
|
+
/** A gate plus its nested rules (`outcomeIf` / `outcomeElseIf`); see `OutcomeRuleNode`. */
|
|
75
|
+
export interface OutcomeConditionBranchNode {
|
|
76
|
+
readonly expression: RpExpressionSchema;
|
|
77
|
+
readonly rules: readonly OutcomeRuleNode[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The recursive outcome-rule shape (a faithful mirror of `OutcomeRuleView` with zod's
|
|
82
|
+
* `T | undefined` optionals). Declared explicitly so `z.infer` stays precise through the
|
|
83
|
+
* recursion (the getter form degrades nested rules to `Record<string, unknown>`).
|
|
84
|
+
*/
|
|
85
|
+
export interface OutcomeRuleNode {
|
|
86
|
+
readonly kind: string;
|
|
87
|
+
readonly identifier?: string | undefined;
|
|
88
|
+
readonly expression?: RpExpressionSchema | undefined;
|
|
89
|
+
readonly rules?: readonly OutcomeRuleNode[] | undefined;
|
|
90
|
+
readonly outcomeIf?: OutcomeConditionBranchNode | undefined;
|
|
91
|
+
readonly outcomeElseIfs?: readonly OutcomeConditionBranchNode[] | undefined;
|
|
92
|
+
readonly outcomeElse?: { readonly rules: readonly OutcomeRuleNode[] } | undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** One `outcomeIf` / `outcomeElseIf` branch: a gate plus its nested rules. */
|
|
96
|
+
export const outcomeConditionBranchSchema: z.ZodType<OutcomeConditionBranchNode> = z.lazy(() =>
|
|
97
|
+
z.object({
|
|
98
|
+
expression: rpExpressionSchema,
|
|
99
|
+
rules: z.array(outcomeRuleSchema),
|
|
100
|
+
}),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
/** A recursive outcome rule (mirrors `OutcomeRuleView`); `kind` stays permissive. */
|
|
104
|
+
export const outcomeRuleSchema: z.ZodType<OutcomeRuleNode> = z.lazy(() =>
|
|
105
|
+
z.object({
|
|
106
|
+
kind: z.string(),
|
|
107
|
+
identifier: z.string().optional(),
|
|
108
|
+
expression: rpExpressionSchema.optional(),
|
|
109
|
+
rules: z.array(outcomeRuleSchema).optional(),
|
|
110
|
+
outcomeIf: outcomeConditionBranchSchema.optional(),
|
|
111
|
+
outcomeElseIfs: z.array(outcomeConditionBranchSchema).optional(),
|
|
112
|
+
outcomeElse: z.object({ rules: z.array(outcomeRuleSchema) }).optional(),
|
|
113
|
+
}),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
export const outcomeProcessingSchema = z.object({ rules: z.array(outcomeRuleSchema) });
|
|
117
|
+
|
|
118
|
+
export type OutcomeProcessingSchema = z.infer<typeof outcomeProcessingSchema>;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* `testFeedback` — the scalar attributes are mirrored; the `content` body is accepted
|
|
122
|
+
* opaquely (BodyNode is the item runtime's content model, not authored at the test level).
|
|
123
|
+
*/
|
|
124
|
+
export const testFeedbackSchema = z.object({
|
|
125
|
+
access: z.enum(["atEnd", "during"]).optional(),
|
|
126
|
+
outcomeIdentifier: z.string().trim().min(1),
|
|
127
|
+
identifier: z.string().trim().min(1),
|
|
128
|
+
showHide: z.enum(["show", "hide"]).optional(),
|
|
129
|
+
content: z.array(z.unknown()).optional(),
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
export type TestFeedbackSchema = z.infer<typeof testFeedbackSchema>;
|
|
133
|
+
|
|
134
|
+
// ---------- The delivery itemRef (href identity) ----------
|
|
135
|
+
|
|
136
|
+
/** The delivery `assessmentItemRef` (`href` identity) — the ready view itemRef. */
|
|
137
|
+
export const assessmentItemRefViewSchema = z.object({
|
|
138
|
+
kind: z.literal("assessmentItemRef"),
|
|
139
|
+
identifier: z.string().trim().min(1),
|
|
140
|
+
href: z.string().optional(),
|
|
141
|
+
categories: z.array(z.string()).optional(),
|
|
142
|
+
fixed: z.boolean().optional(),
|
|
143
|
+
required: z.boolean().optional(),
|
|
144
|
+
preConditions: z.array(rpExpressionSchema).optional(),
|
|
145
|
+
branchRules: z.array(branchRuleSchema).optional(),
|
|
146
|
+
itemSessionControl: itemSessionControlSchema.optional(),
|
|
147
|
+
timeLimits: timeLimitsSchema.optional(),
|
|
148
|
+
weights: z.array(weightSchema).optional(),
|
|
149
|
+
templateDefaults: z.array(templateDefaultSchema).optional(),
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ---------- The recursive structure factory ----------
|
|
153
|
+
|
|
154
|
+
const assessmentSectionFlags = {
|
|
155
|
+
title: z.string().trim().min(1).optional(),
|
|
156
|
+
visible: z.boolean().optional(),
|
|
157
|
+
fixed: z.boolean().optional(),
|
|
158
|
+
required: z.boolean().optional(),
|
|
159
|
+
keepTogether: z.boolean().optional(),
|
|
160
|
+
selection: selectionSchema.optional(),
|
|
161
|
+
ordering: orderingSchema.optional(),
|
|
162
|
+
preConditions: z.array(rpExpressionSchema).optional(),
|
|
163
|
+
branchRules: z.array(branchRuleSchema).optional(),
|
|
164
|
+
itemSessionControl: itemSessionControlSchema.optional(),
|
|
165
|
+
timeLimits: timeLimitsSchema.optional(),
|
|
166
|
+
} as const;
|
|
167
|
+
|
|
168
|
+
const testPartLevel = {
|
|
169
|
+
preConditions: z.array(rpExpressionSchema).optional(),
|
|
170
|
+
branchRules: z.array(branchRuleSchema).optional(),
|
|
171
|
+
itemSessionControl: itemSessionControlSchema.optional(),
|
|
172
|
+
timeLimits: timeLimitsSchema.optional(),
|
|
173
|
+
} as const;
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The recursive section shape, parameterized by the injected itemRef's inferred type.
|
|
177
|
+
* Optionals carry `| undefined` and arrays are `readonly` so this matches zod's object
|
|
178
|
+
* output exactly — letting the `z.lazy` section schema below be annotated `z.ZodType<…>`
|
|
179
|
+
* (a getter cannot self-infer once a generic itemRef joins the `children` union).
|
|
180
|
+
*/
|
|
181
|
+
export interface AssessmentSectionNode<TItemRef> {
|
|
182
|
+
readonly kind: "assessmentSection";
|
|
183
|
+
readonly identifier: string;
|
|
184
|
+
readonly title?: string | undefined;
|
|
185
|
+
readonly visible?: boolean | undefined;
|
|
186
|
+
readonly fixed?: boolean | undefined;
|
|
187
|
+
readonly required?: boolean | undefined;
|
|
188
|
+
readonly keepTogether?: boolean | undefined;
|
|
189
|
+
readonly selection?: SelectionSchema | undefined;
|
|
190
|
+
readonly ordering?: OrderingSchema | undefined;
|
|
191
|
+
readonly preConditions?: readonly RpExpressionSchema[] | undefined;
|
|
192
|
+
readonly branchRules?: readonly BranchRuleSchema[] | undefined;
|
|
193
|
+
readonly itemSessionControl?: ItemSessionControlSchema | undefined;
|
|
194
|
+
readonly timeLimits?: TimeLimitsSchema | undefined;
|
|
195
|
+
readonly children: ReadonlyArray<AssessmentSectionNode<TItemRef> | TItemRef>;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** A test part: navigation/submission modes plus the cascade levels and its sections. */
|
|
199
|
+
export interface TestPartNode<TItemRef> {
|
|
200
|
+
readonly identifier: string;
|
|
201
|
+
readonly navigationMode: "linear" | "nonlinear";
|
|
202
|
+
readonly submissionMode: "individual" | "simultaneous";
|
|
203
|
+
readonly preConditions?: readonly RpExpressionSchema[] | undefined;
|
|
204
|
+
readonly branchRules?: readonly BranchRuleSchema[] | undefined;
|
|
205
|
+
readonly itemSessionControl?: ItemSessionControlSchema | undefined;
|
|
206
|
+
readonly timeLimits?: TimeLimitsSchema | undefined;
|
|
207
|
+
readonly assessmentSections: readonly AssessmentSectionNode<TItemRef>[];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The whole `assessmentTest`: declarations/processing/feedback plus the ordered parts. */
|
|
211
|
+
export interface AssessmentTestNode<TItemRef> {
|
|
212
|
+
readonly identifier: string;
|
|
213
|
+
readonly title?: string | undefined;
|
|
214
|
+
readonly outcomeDeclarations?: readonly OutcomeDeclarationSchema[] | undefined;
|
|
215
|
+
readonly timeLimits?: TimeLimitsSchema | undefined;
|
|
216
|
+
readonly testParts: readonly TestPartNode<TItemRef>[];
|
|
217
|
+
readonly outcomeProcessing?: OutcomeProcessingSchema | undefined;
|
|
218
|
+
readonly testFeedbacks?: readonly TestFeedbackSchema[] | undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** The schema bundle the factory returns (each level parameterized by the itemRef type). */
|
|
222
|
+
export interface AssessmentTestSchemas<TItemRef> {
|
|
223
|
+
readonly assessmentSectionSchema: z.ZodType<AssessmentSectionNode<TItemRef>>;
|
|
224
|
+
readonly testPartSchema: z.ZodType<TestPartNode<TItemRef>>;
|
|
225
|
+
readonly assessmentTestSchema: z.ZodType<AssessmentTestNode<TItemRef>>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Build the recursive `assessmentTest` schema around a caller-supplied itemRef schema.
|
|
230
|
+
* Sections nest sections-or-itemRefs through `children` (recursion via `z.lazy`); `z.infer`
|
|
231
|
+
* follows the recursion and carries the injected itemRef's inferred type. The explicit
|
|
232
|
+
* return type keeps the package's `isolatedDeclarations` build able to emit the bundle.
|
|
233
|
+
*/
|
|
234
|
+
export function makeAssessmentTestSchema<ItemRef extends z.ZodType>(
|
|
235
|
+
itemRefSchema: ItemRef,
|
|
236
|
+
): AssessmentTestSchemas<z.infer<ItemRef>> {
|
|
237
|
+
const assessmentSectionSchema: z.ZodType<AssessmentSectionNode<z.infer<ItemRef>>> = z.lazy(() =>
|
|
238
|
+
z.object({
|
|
239
|
+
kind: z.literal("assessmentSection"),
|
|
240
|
+
identifier: z.string().trim().min(1),
|
|
241
|
+
...assessmentSectionFlags,
|
|
242
|
+
children: z.array(z.union([assessmentSectionSchema, itemRefSchema])),
|
|
243
|
+
}),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const testPartSchema: z.ZodType<TestPartNode<z.infer<ItemRef>>> = z.object({
|
|
247
|
+
identifier: z.string().trim().min(1),
|
|
248
|
+
navigationMode: z.enum(["linear", "nonlinear"]),
|
|
249
|
+
submissionMode: z.enum(["individual", "simultaneous"]),
|
|
250
|
+
...testPartLevel,
|
|
251
|
+
assessmentSections: z.array(assessmentSectionSchema),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const assessmentTestSchema: z.ZodType<AssessmentTestNode<z.infer<ItemRef>>> = z.object({
|
|
255
|
+
identifier: z.string().trim().min(1),
|
|
256
|
+
title: z.string().trim().min(1).optional(),
|
|
257
|
+
outcomeDeclarations: z.array(outcomeDeclarationSchema).optional(),
|
|
258
|
+
timeLimits: timeLimitsSchema.optional(),
|
|
259
|
+
testParts: z.array(testPartSchema),
|
|
260
|
+
outcomeProcessing: outcomeProcessingSchema.optional(),
|
|
261
|
+
testFeedbacks: z.array(testFeedbackSchema).optional(),
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
return { assessmentSectionSchema, testPartSchema, assessmentTestSchema };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** The ready delivery-view schema: the recursive structure over the `href` itemRef. */
|
|
268
|
+
export const assessmentTestViewSchema: z.ZodType<AssessmentTestNode<AssessmentItemRefViewSchema>> =
|
|
269
|
+
makeAssessmentTestSchema(assessmentItemRefViewSchema).assessmentTestSchema;
|
|
270
|
+
|
|
271
|
+
export type AssessmentItemRefViewSchema = z.infer<typeof assessmentItemRefViewSchema>;
|
|
272
|
+
export type AssessmentTestViewSchema = z.infer<typeof assessmentTestViewSchema>;
|