@conform-ed/qti-react 0.0.12 → 0.0.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/dist/capability.d.ts +17 -0
  2. package/dist/content-model.d.ts +42 -0
  3. package/dist/graphic.d.ts +23 -0
  4. package/dist/index.d.ts +14 -0
  5. package/dist/index.js +4556 -212
  6. package/dist/interactions/associate.d.ts +2 -0
  7. package/dist/interactions/choice.d.ts +2 -0
  8. package/dist/interactions/drawing.d.ts +2 -0
  9. package/dist/interactions/end-attempt.d.ts +2 -0
  10. package/dist/interactions/extended-text.d.ts +2 -0
  11. package/dist/interactions/gap-match.d.ts +2 -0
  12. package/dist/interactions/graphic.d.ts +13 -0
  13. package/dist/interactions/hottext.d.ts +2 -0
  14. package/dist/interactions/index.d.ts +18 -0
  15. package/dist/interactions/inline-choice.d.ts +2 -0
  16. package/dist/interactions/match.d.ts +2 -0
  17. package/dist/interactions/media.d.ts +2 -0
  18. package/dist/interactions/order.d.ts +2 -0
  19. package/dist/interactions/slider.d.ts +2 -0
  20. package/dist/interactions/text-entry.d.ts +2 -0
  21. package/dist/interactions/upload.d.ts +2 -0
  22. package/dist/normalized-item.d.ts +30 -0
  23. package/dist/pci/index.d.ts +6 -0
  24. package/dist/pci/interaction.d.ts +8 -0
  25. package/dist/pci/markup.d.ts +10 -0
  26. package/dist/pci/mount.d.ts +50 -0
  27. package/dist/pci/registry.d.ts +53 -0
  28. package/dist/pci/response.d.ts +11 -0
  29. package/dist/pci/skin.d.ts +12 -0
  30. package/dist/reference-skin/associate.d.ts +8 -0
  31. package/dist/reference-skin/choice.d.ts +8 -0
  32. package/dist/reference-skin/content.d.ts +6 -0
  33. package/dist/reference-skin/drawing.d.ts +9 -0
  34. package/dist/reference-skin/end-attempt.d.ts +7 -0
  35. package/dist/reference-skin/extended-text.d.ts +6 -0
  36. package/dist/reference-skin/gap-match.d.ts +8 -0
  37. package/dist/reference-skin/graphic-associate.d.ts +8 -0
  38. package/dist/reference-skin/graphic-base.d.ts +39 -0
  39. package/dist/reference-skin/graphic-gap-match.d.ts +8 -0
  40. package/dist/reference-skin/graphic-order.d.ts +8 -0
  41. package/dist/reference-skin/hotspot.d.ts +8 -0
  42. package/dist/reference-skin/hottext.d.ts +8 -0
  43. package/dist/reference-skin/index.d.ts +30 -0
  44. package/dist/reference-skin/inline-choice.d.ts +7 -0
  45. package/dist/reference-skin/match.d.ts +8 -0
  46. package/dist/reference-skin/media.d.ts +9 -0
  47. package/dist/reference-skin/order.d.ts +8 -0
  48. package/dist/reference-skin/position-object.d.ts +9 -0
  49. package/dist/reference-skin/select-point.d.ts +8 -0
  50. package/dist/reference-skin/slider.d.ts +8 -0
  51. package/dist/reference-skin/text-entry.d.ts +6 -0
  52. package/dist/reference-skin/upload.d.ts +8 -0
  53. package/dist/response-processing.d.ts +48 -0
  54. package/dist/rp/evaluate.d.ts +35 -0
  55. package/dist/rp/index.d.ts +4 -0
  56. package/dist/rp/interpreter.d.ts +15 -0
  57. package/dist/rp/template-processing.d.ts +49 -0
  58. package/dist/rp/templates.d.ts +8 -0
  59. package/dist/rp/types.d.ts +158 -0
  60. package/dist/rp/values.d.ts +27 -0
  61. package/dist/runtime.d.ts +164 -0
  62. package/dist/store.d.ts +61 -0
  63. package/dist/test/controller.d.ts +11 -0
  64. package/dist/test/index.d.ts +3 -0
  65. package/dist/test/session-store.d.ts +46 -0
  66. package/dist/test/types.d.ts +194 -0
  67. package/dist/types.d.ts +58 -0
  68. package/package.json +8 -6
  69. package/src/capability.ts +24 -0
  70. package/src/content-model.ts +104 -5
  71. package/src/graphic.ts +103 -0
  72. package/src/index.ts +139 -3
  73. package/src/interactions/associate.ts +22 -0
  74. package/src/interactions/choice.ts +2 -2
  75. package/src/interactions/drawing.ts +24 -0
  76. package/src/interactions/end-attempt.ts +19 -0
  77. package/src/interactions/extended-text.ts +21 -0
  78. package/src/interactions/gap-match.ts +22 -0
  79. package/src/interactions/graphic.ts +104 -0
  80. package/src/interactions/hottext.ts +21 -0
  81. package/src/interactions/index.ts +57 -3
  82. package/src/interactions/inline-choice.ts +2 -2
  83. package/src/interactions/match.ts +27 -0
  84. package/src/interactions/media.ts +24 -0
  85. package/src/interactions/order.ts +21 -0
  86. package/src/interactions/slider.ts +24 -0
  87. package/src/interactions/text-entry.ts +2 -2
  88. package/src/interactions/upload.ts +19 -0
  89. package/src/normalized-item.ts +563 -0
  90. package/src/pci/index.ts +22 -0
  91. package/src/pci/interaction.ts +42 -0
  92. package/src/pci/markup.ts +102 -0
  93. package/src/pci/mount.ts +134 -0
  94. package/src/pci/registry.ts +240 -0
  95. package/src/pci/response.ts +138 -0
  96. package/src/pci/skin.ts +86 -0
  97. package/src/reference-skin/associate.ts +98 -0
  98. package/src/reference-skin/choice.ts +44 -0
  99. package/src/reference-skin/content.ts +30 -0
  100. package/src/reference-skin/drawing.ts +160 -0
  101. package/src/reference-skin/end-attempt.ts +27 -0
  102. package/src/reference-skin/extended-text.ts +35 -0
  103. package/src/reference-skin/gap-match.ts +69 -0
  104. package/src/reference-skin/graphic-associate.ts +123 -0
  105. package/src/reference-skin/graphic-base.ts +142 -0
  106. package/src/reference-skin/graphic-gap-match.ts +143 -0
  107. package/src/reference-skin/graphic-order.ts +76 -0
  108. package/src/reference-skin/hotspot.ts +43 -0
  109. package/src/reference-skin/hottext.ts +42 -0
  110. package/src/reference-skin/index.ts +74 -0
  111. package/src/reference-skin/inline-choice.ts +42 -0
  112. package/src/reference-skin/match.ts +80 -0
  113. package/src/reference-skin/media.ts +74 -0
  114. package/src/reference-skin/order.ts +79 -0
  115. package/src/reference-skin/position-object.ts +84 -0
  116. package/src/reference-skin/select-point.ts +87 -0
  117. package/src/reference-skin/slider.ts +41 -0
  118. package/src/reference-skin/text-entry.ts +31 -0
  119. package/src/reference-skin/upload.ts +46 -0
  120. package/src/response-processing.ts +178 -29
  121. package/src/rp/evaluate.ts +827 -0
  122. package/src/rp/index.ts +30 -0
  123. package/src/rp/interpreter.ts +254 -0
  124. package/src/rp/template-processing.ts +290 -0
  125. package/src/rp/templates.ts +190 -0
  126. package/src/rp/types.ts +167 -0
  127. package/src/rp/values.ts +211 -0
  128. package/src/runtime.ts +476 -28
  129. package/src/store.ts +161 -5
  130. package/src/test/controller.ts +809 -0
  131. package/src/test/index.ts +25 -0
  132. package/src/test/session-store.ts +243 -0
  133. package/src/test/types.ts +203 -0
  134. package/src/types.ts +27 -1
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The shared QTI expression evaluator (ADR-0004), used by both response processing and
3
+ * template processing. The environment supplies variable lookup, declarations, and —
4
+ * only where the spec allows nondeterminism (template processing) — a seeded PRNG.
5
+ * Random operators without a PRNG in the environment are unsupported constructs:
6
+ * response processing must stay deterministic and replayable.
7
+ */
8
+ import type { ResponseDeclarationView, ResponseValue } from "../types";
9
+ import type { CustomOperatorImplementation, ResponseNormalization, RpExpressionView } from "./types";
10
+ import { type MaybeRpValue } from "./values";
11
+ export interface EvalEnv {
12
+ readonly lookupVariable: (identifier: string) => MaybeRpValue;
13
+ readonly responseDeclaration: (identifier: string) => ResponseDeclarationView | undefined;
14
+ readonly responseValue: (identifier: string) => ResponseValue;
15
+ readonly normalization?: ResponseNormalization | undefined;
16
+ /** Seeded PRNG in [0, 1); present only in template processing. */
17
+ readonly random?: (() => number) | undefined;
18
+ /** `testVariables` aggregation; present only in test-level outcome processing. */
19
+ readonly testVariables?: (expression: RpExpressionView) => MaybeRpValue;
20
+ /** The `number*` item-session aggregates; present only in test-level outcome processing. */
21
+ readonly testAggregate?: (expression: RpExpressionView) => MaybeRpValue;
22
+ /** Registered vendor operators by class; unregistered classes stay unsupported. */
23
+ readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>> | undefined;
24
+ }
25
+ /** Expression kinds legal everywhere (deterministic). */
26
+ export declare const deterministicExpressionKinds: ReadonlySet<string>;
27
+ /** Expression kinds requiring the seeded PRNG (template processing only). */
28
+ export declare const randomExpressionKinds: ReadonlySet<string>;
29
+ export declare class RpUnsupportedError extends Error {
30
+ readonly kindName: string;
31
+ constructor(kindName: string);
32
+ }
33
+ export declare function evaluateExpression(expression: RpExpressionView, env: EvalEnv): MaybeRpValue;
34
+ /** Walk an expression tree, reporting kinds outside the allowed set. */
35
+ export declare function collectExpressionIssues(expression: RpExpressionView, allowedKinds: ReadonlySet<string>, report: (name: string) => void, customOperatorClasses?: ReadonlySet<string>): void;
@@ -0,0 +1,4 @@
1
+ export { collectRpIssues, executeResponseProcessing } from "./interpreter";
2
+ export { applyCorrectResponseOverrides, collectTemplateIssues, executeTemplateProcessing, mulberry32, type TemplateConditionBranch, type TemplateProcessingContext, type TemplateProcessingResult, type TemplateProcessingView, type TemplateRuleView, } from "./template-processing";
3
+ export { resolveTemplate } from "./templates";
4
+ export type { CustomOperatorImplementation, MaybeRpValue, OutcomeDeclarationView, OutcomeValue, ResponseNormalization, ResponseProcessingContext, ResponseProcessingResult, ResponseProcessingView, RpConditionBranch, RpExpressionView, RpRecordField, RpRuleView, RpScalar, RpValue, TemplateDeclarationView, } from "./types";
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The Response Processing Interpreter (ADR-0004): a pure, deterministic evaluator of
3
+ * the contracts-validated `responseProcessing` tree. Operator coverage grows milestone
4
+ * by milestone; anything outside it aborts execution to the declared outcome defaults
5
+ * and is reported as an `unsupported-rp` Capability issue — never partial scoring.
6
+ */
7
+ import type { CapabilityIssue } from "../capability";
8
+ import type { ResponseProcessingContext, ResponseProcessingResult, ResponseProcessingView } from "./types";
9
+ export declare function executeResponseProcessing(view: ResponseProcessingView | undefined, context: ResponseProcessingContext): ResponseProcessingResult;
10
+ export interface RpIssueOptions {
11
+ /** `customOperator` classes the consumer has registered implementations for. */
12
+ readonly customOperatorClasses?: ReadonlySet<string>;
13
+ }
14
+ /** Static coverage walk for `canDeliver`: reports constructs the interpreter lacks without executing. */
15
+ export declare function collectRpIssues(view: ResponseProcessingView | undefined, options?: RpIssueOptions): readonly CapabilityIssue[];
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Template processing (ADR-0004): the seeded, deterministic engine that produces an
3
+ * item clone. Given the same seed, the same template values and correctResponse
4
+ * overrides come out — replayability survives randomized items because the seed, not
5
+ * the outcome, is what gets stored. Supported rules: setTemplateValue,
6
+ * templateCondition, setCorrectResponse, exitTemplate.
7
+ */
8
+ import type { CapabilityIssue } from "../capability";
9
+ import type { CorrectResponseView, ResponseDeclarationView } from "../types";
10
+ import type { CustomOperatorImplementation, OutcomeValue, RpExpressionView, TemplateDeclarationView } from "./types";
11
+ export interface TemplateConditionBranch {
12
+ readonly expression: RpExpressionView;
13
+ readonly rules: readonly TemplateRuleView[];
14
+ }
15
+ export interface TemplateRuleView {
16
+ readonly kind: string;
17
+ readonly identifier?: string;
18
+ readonly expression?: RpExpressionView;
19
+ readonly templateIf?: TemplateConditionBranch;
20
+ readonly templateElseIfs?: readonly TemplateConditionBranch[];
21
+ readonly templateElse?: {
22
+ readonly rules: readonly TemplateRuleView[];
23
+ };
24
+ }
25
+ export interface TemplateProcessingView {
26
+ readonly rules: readonly TemplateRuleView[];
27
+ }
28
+ export interface TemplateProcessingContext {
29
+ readonly templateDeclarations: readonly TemplateDeclarationView[];
30
+ readonly responseDeclarations: readonly ResponseDeclarationView[];
31
+ readonly seed: number;
32
+ /** Registered vendor `customOperator` implementations by class (opt-in). */
33
+ readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>> | undefined;
34
+ }
35
+ export interface TemplateProcessingResult {
36
+ readonly templateValues: Readonly<Record<string, OutcomeValue>>;
37
+ /** correctResponse values set by setCorrectResponse, keyed by response identifier. */
38
+ readonly correctResponseOverrides: Readonly<Record<string, CorrectResponseView>>;
39
+ readonly issues: readonly CapabilityIssue[];
40
+ }
41
+ /** mulberry32: a tiny, fast, seeded PRNG — deterministic across platforms. */
42
+ export declare function mulberry32(seed: number): () => number;
43
+ export declare function executeTemplateProcessing(view: TemplateProcessingView | undefined, context: TemplateProcessingContext): TemplateProcessingResult;
44
+ /** The effective response declarations for a clone: setCorrectResponse overrides applied. */
45
+ export declare function applyCorrectResponseOverrides(declarations: readonly ResponseDeclarationView[], overrides: Readonly<Record<string, CorrectResponseView>>): readonly ResponseDeclarationView[];
46
+ /** Static coverage walk for `canDeliver` over a templateProcessing tree. */
47
+ export declare function collectTemplateIssues(view: TemplateProcessingView | undefined, options?: {
48
+ readonly customOperatorClasses?: ReadonlySet<string>;
49
+ }): readonly CapabilityIssue[];
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The QTI standard response-processing templates as built-in canonical rule trees
3
+ * (ADR-0004): templates are interpreter inputs, not a separate scoring path. URIs are
4
+ * matched by their final path segment, so both the QTI 2.x and 3.0 purl forms resolve.
5
+ */
6
+ import type { RpRuleView } from "./types";
7
+ /** Resolve a standard-template URI to its canonical rules, or null when unknown. */
8
+ export declare function resolveTemplate(uri: string): readonly RpRuleView[] | null;
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Structural views of QTI 3 `responseProcessing` and `outcomeDeclaration` (ADR-0004).
3
+ * Like the rest of the runtime's views, these are narrowed shapes of the
4
+ * `@conform-ed/contracts` schemas: validation happens upstream, the interpreter works
5
+ * against these types and reports anything it does not understand via the Capability
6
+ * Report instead of guessing.
7
+ */
8
+ import type { CapabilityIssue } from "../capability";
9
+ import type { Cardinality, ResponseDeclarationView, ResponseValue } from "../types";
10
+ export type RpScalar = string | number | boolean;
11
+ /** An outcome variable's value as exposed to consumers after response processing. */
12
+ export type OutcomeValue = RpScalar | readonly RpScalar[] | null;
13
+ /** One named member of a record-cardinality value (QTI record containers). */
14
+ export interface RpRecordField {
15
+ readonly name: string;
16
+ readonly baseType?: string;
17
+ readonly value: RpScalar;
18
+ }
19
+ /** The interpreter's typed value model: (baseType, cardinality, members); NULL is null. */
20
+ export interface RpValue {
21
+ readonly cardinality: Cardinality;
22
+ readonly baseType?: string;
23
+ readonly values: readonly RpScalar[];
24
+ /** Present only when cardinality is "record"; `values` mirrors the field values. */
25
+ readonly fields?: readonly RpRecordField[];
26
+ }
27
+ export type MaybeRpValue = RpValue | null;
28
+ export interface OutcomeDeclarationView {
29
+ readonly identifier: string;
30
+ readonly cardinality: Cardinality;
31
+ readonly baseType?: string;
32
+ readonly defaultValue?: {
33
+ readonly values: ReadonlyArray<{
34
+ readonly value: RpScalar;
35
+ }>;
36
+ };
37
+ }
38
+ /**
39
+ * One expression node. Deliberately loose (`kind: string`): kinds the interpreter
40
+ * does not implement yet flow through and surface as `unsupported-rp` issues.
41
+ */
42
+ export interface RpExpressionView {
43
+ readonly kind: string;
44
+ readonly identifier?: string;
45
+ readonly baseType?: string;
46
+ readonly value?: RpScalar;
47
+ readonly expressions?: readonly RpExpressionView[];
48
+ /** Bounds/step for the random operators (template processing). */
49
+ readonly min?: number;
50
+ readonly max?: number;
51
+ readonly step?: number;
52
+ /** `equal` tolerance window; string entries are template references (unsupported). */
53
+ readonly toleranceMode?: "exact" | "absolute" | "relative";
54
+ readonly tolerance?: ReadonlyArray<number | string>;
55
+ readonly includeLowerBound?: boolean;
56
+ readonly includeUpperBound?: boolean;
57
+ /** 1-based position for `index`. */
58
+ readonly n?: number | string;
59
+ /** Function/constant name for `mathOperator`, `mathConstant`, `statsOperator`. */
60
+ readonly name?: string;
61
+ /** Rounding controls for `roundTo` and `equalRounded`. */
62
+ readonly roundingMode?: "decimalPlaces" | "significantFigures";
63
+ readonly figures?: number | string;
64
+ /** Pass count for `repeat`. */
65
+ readonly numberRepeats?: number | string;
66
+ /** String comparison controls for `stringMatch` and `substring`. */
67
+ readonly caseSensitive?: boolean;
68
+ readonly substring?: boolean;
69
+ /** Area for `inside` (QTI shape + coords string). */
70
+ readonly shape?: string;
71
+ readonly coords?: string;
72
+ /** Test-level subset selection (`testVariables` and the `number*` aggregates). */
73
+ readonly variableIdentifier?: string;
74
+ readonly weightIdentifier?: string;
75
+ readonly sectionIdentifier?: string;
76
+ readonly includeCategory?: string | readonly string[];
77
+ readonly excludeCategory?: string | readonly string[];
78
+ /** Vendor identification for `customOperator` (implementation registered by class). */
79
+ readonly class?: string;
80
+ readonly definition?: string;
81
+ /** Named-field selector for `fieldValue` over record-cardinality values. */
82
+ readonly fieldIdentifier?: string;
83
+ }
84
+ /**
85
+ * A consumer-registered `customOperator` implementation, keyed by its `class`
86
+ * attribute. Receives the already-evaluated child values; returns NULL-or-value like
87
+ * any expression. Vendor operators are by definition engine-specific (the spec leaves
88
+ * them implementation-defined), so nothing ships registered — same opt-in stance as
89
+ * PCI modules.
90
+ */
91
+ export type CustomOperatorImplementation = (args: readonly MaybeRpValue[], expression: RpExpressionView) => MaybeRpValue;
92
+ export interface RpConditionBranch {
93
+ readonly expression: RpExpressionView;
94
+ readonly rules: readonly RpRuleView[];
95
+ }
96
+ /** One response rule: responseCondition, setOutcomeValue, or exitResponse. */
97
+ export interface RpRuleView {
98
+ readonly kind: string;
99
+ readonly identifier?: string;
100
+ readonly expression?: RpExpressionView;
101
+ readonly responseIf?: RpConditionBranch;
102
+ readonly responseElseIfs?: readonly RpConditionBranch[];
103
+ readonly responseElse?: {
104
+ readonly rules: readonly RpRuleView[];
105
+ };
106
+ }
107
+ /** Either a standard-template URI or an explicit rule tree (rules win if both). */
108
+ export interface ResponseProcessingView {
109
+ readonly template?: string;
110
+ readonly rules?: readonly RpRuleView[];
111
+ }
112
+ /**
113
+ * Response Normalization (ADR-0004): an opt-in, consumer-configured transform of
114
+ * candidate string input applied at comparison time. Off by default; always off in
115
+ * conformance runs. Applied to both sides of string comparisons so keys and candidate
116
+ * input normalize identically.
117
+ */
118
+ export type ResponseNormalization = (value: string, declaration?: ResponseDeclarationView) => string;
119
+ export interface TemplateDeclarationView {
120
+ readonly identifier: string;
121
+ readonly cardinality: Cardinality;
122
+ readonly baseType?: string;
123
+ readonly defaultValue?: {
124
+ readonly values: ReadonlyArray<{
125
+ readonly value: RpScalar;
126
+ }>;
127
+ };
128
+ }
129
+ /**
130
+ * Consumer-supplied input. Optional members deliberately admit explicit
131
+ * `undefined` ("undefined means not provided"): callers pass maybe-undefined
132
+ * values straight through, and the interpreter reads every member field-wise
133
+ * (`??`/`?.`) — option bags are never spread-merged over defaults.
134
+ */
135
+ export interface ResponseProcessingContext {
136
+ readonly responseDeclarations: readonly ResponseDeclarationView[];
137
+ readonly outcomeDeclarations: readonly OutcomeDeclarationView[];
138
+ readonly responses: Readonly<Record<string, ResponseValue>>;
139
+ readonly normalization?: ResponseNormalization | undefined;
140
+ /** Template variables for this clone (read by `variable` lookups). */
141
+ readonly templateDeclarations?: readonly TemplateDeclarationView[] | undefined;
142
+ readonly templateValues?: Readonly<Record<string, OutcomeValue>> | undefined;
143
+ /** Adaptive carry-over: outcome values from earlier attempts replace declared defaults. */
144
+ readonly priorOutcomes?: Readonly<Record<string, OutcomeValue>> | undefined;
145
+ /**
146
+ * Random source for `random`/`randomInteger`/`randomFloat` in RP (adaptive items,
147
+ * e.g. Monty Hall door reveals). Seed it from the attempt seed: the seed plus the
148
+ * submission sequence then replays the exact same outcomes (ADR-0004 determinism).
149
+ */
150
+ readonly random?: (() => number) | undefined;
151
+ /** Registered vendor `customOperator` implementations by class (opt-in). */
152
+ readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>> | undefined;
153
+ }
154
+ export interface ResponseProcessingResult {
155
+ readonly outcomes: Readonly<Record<string, OutcomeValue>>;
156
+ /** Non-empty means execution aborted to declared defaults (never partial scoring). */
157
+ readonly issues: readonly CapabilityIssue[];
158
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The interpreter's typed value model. QTI runtime values are (baseType, cardinality,
3
+ * members); NULL is represented as `null`. Comparison is baseType-aware: pairs are
4
+ * unordered within the pair, directedPairs ordered, strings normalize through the
5
+ * consumer's Response Normalization hook (identity by default), numbers compare
6
+ * numerically.
7
+ */
8
+ import type { Cardinality, ResponseDeclarationView, ResponseValue } from "../types";
9
+ import type { MaybeRpValue, OutcomeValue, ResponseNormalization, RpScalar, RpValue } from "./types";
10
+ export type { MaybeRpValue, RpValue } from "./types";
11
+ export declare function isNumericBaseType(baseType: string | undefined): boolean;
12
+ /** Coerce a raw scalar to its declared baseType (numbers and booleans may arrive as strings). */
13
+ export declare function coerceScalar(value: RpScalar, baseType: string | undefined): RpScalar;
14
+ /** Lift a store ResponseValue into the typed model using its declaration. */
15
+ export declare function fromResponse(declaration: ResponseDeclarationView, response: ResponseValue): MaybeRpValue;
16
+ export declare function singleNumber(value: MaybeRpValue): number | null;
17
+ export declare function singleBoolean(value: MaybeRpValue): boolean | null;
18
+ /** Construct a typed value; an unknown baseType is omitted, never stored as undefined. */
19
+ export declare function rpValue(cardinality: Cardinality, values: readonly RpScalar[], baseType?: string): RpValue;
20
+ export declare function booleanValue(value: boolean): RpValue;
21
+ export declare function floatValue(value: number): RpValue;
22
+ export declare function scalarsEqual(a: RpScalar, b: RpScalar, baseType: string | undefined, normalize?: ResponseNormalization): boolean;
23
+ /** QTI `match`: same cardinality; ordered compares sequences, containers compare multisets. */
24
+ export declare function valuesMatch(a: RpValue, b: RpValue, normalize?: ResponseNormalization): boolean;
25
+ /** Rebuild a typed value from a flattened OutcomeValue (templates, adaptive carry-over). */
26
+ export declare function fromFlatValue(value: OutcomeValue, cardinality: Cardinality, baseType?: string): MaybeRpValue;
27
+ export declare function toOutcomeValue(value: MaybeRpValue): OutcomeValue;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * The headless runtime (ADR-0001): a factory that assembles a QTI item renderer from an
3
+ * injected set of interaction descriptors plus a skin registry. The kind-union is the
4
+ * injected set — no global registry, no module augmentation. The core owns response
5
+ * state and a11y wiring; skins are controlled components.
6
+ *
7
+ * No Mantine, and no JSX — flow content and skins are composed with `createElement` so
8
+ * the package needs no JSX build step.
9
+ */
10
+ import { type ComponentType, type ReactNode } from "react";
11
+ import type { ZodType } from "zod";
12
+ import type { CapabilityReport } from "./capability";
13
+ import { type ContentModel } from "./content-model";
14
+ import type { CustomOperatorImplementation, OutcomeDeclarationView, OutcomeValue, ResponseNormalization, ResponseProcessingView, TemplateDeclarationView, TemplateProcessingView } from "./rp";
15
+ import { type AttemptSnapshot, type AttemptStore } from "./store";
16
+ import type { ResponseDeclarationView, ResponseValue, ScoreResult } from "./types";
17
+ export type { CapabilityIssue, CapabilityIssueType, CapabilityReport } from "./capability";
18
+ export interface XmlContentNode {
19
+ kind: "xml";
20
+ name: string;
21
+ value?: string;
22
+ attributes?: Record<string, unknown>;
23
+ children?: BodyNode[];
24
+ }
25
+ export interface InteractionNode {
26
+ kind: string;
27
+ responseIdentifier: string;
28
+ [field: string]: unknown;
29
+ }
30
+ export type BodyNode = XmlContentNode | InteractionNode | {
31
+ kind: string;
32
+ value?: string;
33
+ children?: BodyNode[];
34
+ };
35
+ /** A feedback element's view: feedbackInline/feedbackBlock in the body, or modalFeedback. */
36
+ export interface FeedbackView {
37
+ outcomeIdentifier: string;
38
+ identifier: string;
39
+ showHide?: "show" | "hide";
40
+ content?: readonly BodyNode[];
41
+ }
42
+ export interface AssessmentItemView {
43
+ responseDeclarations: readonly ResponseDeclarationView[];
44
+ outcomeDeclarations?: readonly OutcomeDeclarationView[];
45
+ responseProcessing?: ResponseProcessingView;
46
+ templateDeclarations?: readonly TemplateDeclarationView[];
47
+ templateProcessing?: TemplateProcessingView;
48
+ /** QTI adaptive item: multiple attempts until completionStatus reaches "completed". */
49
+ adaptive?: boolean;
50
+ modalFeedbacks?: readonly FeedbackView[];
51
+ itemBody: {
52
+ content?: BodyNode[];
53
+ };
54
+ }
55
+ export interface InteractionDescriptor<Kind extends string = string> {
56
+ readonly kind: Kind;
57
+ readonly schema: ZodType;
58
+ readonly scoring: "qti-standard";
59
+ /** The empty/initial response for a fresh attempt at this interaction. */
60
+ initialResponse(node: InteractionNode): ResponseValue;
61
+ }
62
+ export declare function defineInteraction<Kind extends string>(descriptor: InteractionDescriptor<Kind>): InteractionDescriptor<Kind>;
63
+ export type OptionStatus = "idle" | "selected" | "correct" | "incorrect";
64
+ /** Whole-interaction status (for interactions without options, e.g. textEntry). */
65
+ export type InteractionStatus = "unanswered" | "answered" | "correct" | "incorrect";
66
+ /** Prop-getter result a skin spreads onto an option element to inherit selection + a11y. */
67
+ export interface OptionProps {
68
+ role: "radio" | "checkbox";
69
+ tabIndex: number;
70
+ "aria-checked": boolean;
71
+ "aria-disabled": boolean;
72
+ "data-status": OptionStatus;
73
+ onClick: () => void;
74
+ }
75
+ /** Controlled props every interaction skin receives by default (ADR-0001). */
76
+ export interface InteractionRenderProps {
77
+ node: InteractionNode;
78
+ responseIdentifier: string;
79
+ value: ResponseValue;
80
+ setValue: (value: ResponseValue) => void;
81
+ /** True after submit — the interaction is read-only. */
82
+ disabled: boolean;
83
+ /** True after submit — show correct/incorrect chrome. */
84
+ showFeedback: boolean;
85
+ /** Whole-interaction status (drives feedback for option-less interactions). */
86
+ status: InteractionStatus;
87
+ getOptionProps: (optionIdentifier: string) => OptionProps;
88
+ /**
89
+ * Render body fragments (prompt, choice labels) through the core allowlist walk.
90
+ * `overrides` lets a skin take over specific node kinds nested anywhere in the
91
+ * fragment (e.g. `hottext`, `gap`) while the core keeps walking everything else.
92
+ */
93
+ renderContent: (nodes: readonly BodyNode[] | undefined, overrides?: NodeOverrides) => ReactNode;
94
+ /** The runtime's Asset Resolver (identity when none configured) for skin-owned media. */
95
+ resolveAsset: (href: string) => string;
96
+ /** Set this interaction's response to true and submit the attempt (endAttemptInteraction). */
97
+ endAttempt: () => void;
98
+ /**
99
+ * Register a submit-time response collector for this interaction (imperative
100
+ * interactions like PCI own their response state). Returns the unregister function.
101
+ */
102
+ registerResponseCollector: (collector: () => ResponseValue | undefined) => () => void;
103
+ }
104
+ /** Per-kind render overrides a skin passes to `renderContent` for nodes it owns. */
105
+ export type NodeOverrides = Readonly<Record<string, (node: BodyNode, key: number) => ReactNode>>;
106
+ export type InteractionSkin = ComponentType<InteractionRenderProps>;
107
+ export type SkinRegistry = Readonly<Record<string, InteractionSkin>>;
108
+ export interface QtiRuntimeConfig {
109
+ readonly interactions: readonly InteractionDescriptor[];
110
+ readonly skin: SkinRegistry;
111
+ readonly contentModel?: ContentModel;
112
+ /** Replaces the default Unsupported Placeholder for interaction nodes this runtime cannot render. */
113
+ readonly renderUnsupported?: (node: InteractionNode) => ReactNode;
114
+ /** The Response Normalization hook (ADR-0004): opt-in candidate-input leniency. */
115
+ readonly normalization?: ResponseNormalization;
116
+ /**
117
+ * The Asset Resolver: maps package-relative media references (img/audio/video `src`,
118
+ * `poster`) to real URLs at render time. Identity when omitted.
119
+ */
120
+ readonly assetResolver?: (href: string) => string;
121
+ /**
122
+ * Registered vendor `customOperator` implementations by class. Items using only
123
+ * registered classes pass the capability gate; everything else stays unsupported.
124
+ */
125
+ readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>>;
126
+ }
127
+ export interface ItemRendererProps {
128
+ item: AssessmentItemView;
129
+ /**
130
+ * An externally owned attempt store. Without it the renderer creates a fresh
131
+ * per-mount store; passing one enables review/replay modes (rehydrate a stored,
132
+ * already-submitted attempt) and server-side rendering of submitted states.
133
+ */
134
+ store?: AttemptStore | undefined;
135
+ /** Clone seed for template processing; store it to replay the same clone. */
136
+ seed?: number | undefined;
137
+ children?: ReactNode;
138
+ }
139
+ export interface ContentRendererProps {
140
+ nodes?: readonly BodyNode[] | undefined;
141
+ /** Values for printedVariable (and showHide-gated feedback) inside the content. */
142
+ outcomes?: Readonly<Record<string, OutcomeValue>> | undefined;
143
+ }
144
+ export interface QtiRuntime {
145
+ ItemRenderer: ComponentType<ItemRendererProps>;
146
+ /**
147
+ * Flow content outside an item attempt — test feedback, rubric copy. Same sanitizer
148
+ * and node walk as the item body, over caller-supplied outcome values.
149
+ */
150
+ ContentRenderer: ComponentType<ContentRendererProps>;
151
+ useAttempt: () => AttemptController;
152
+ /**
153
+ * The Capability Report for an item against this runtime's injected descriptors,
154
+ * skins, and content model. Consumers gate delivery on it (ADR-0003); the
155
+ * ItemRenderer placeholder is only the backstop for content that reaches
156
+ * rendering anyway.
157
+ */
158
+ canDeliver: (item: AssessmentItemView) => CapabilityReport;
159
+ }
160
+ export interface AttemptController extends AttemptSnapshot {
161
+ submit: () => readonly ScoreResult[];
162
+ reset: () => void;
163
+ }
164
+ export declare function createQtiRuntime(config: QtiRuntimeConfig): QtiRuntime;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The response store the headless core owns (ADR-0001): it holds candidate responses
3
+ * keyed by `responseIdentifier`, the submitted flag, and the scored outcomes. Skins are
4
+ * controlled against it; they never own response state (only ephemeral UI state).
5
+ *
6
+ * Backed by an external store so `useSyncExternalStore` can subscribe with narrow,
7
+ * snapshot-stable reads. No React import here — the hook lives in the runtime.
8
+ *
9
+ * Template processing runs once at store creation under the given seed (ADR-0004):
10
+ * the seed is the replay key for a randomized clone. Adaptive items (`adaptive`)
11
+ * support multiple attempts: outcomes carry over between RP runs and the item locks
12
+ * only when `completionStatus` reaches "completed".
13
+ */
14
+ import type { CustomOperatorImplementation, OutcomeDeclarationView, OutcomeValue, ResponseNormalization, ResponseProcessingView, TemplateDeclarationView, TemplateProcessingView } from "./rp";
15
+ import type { ResponseDeclarationView, ResponseValue, ScoreResult } from "./types";
16
+ export interface AttemptSnapshot {
17
+ readonly responses: Readonly<Record<string, ResponseValue>>;
18
+ readonly submitted: boolean;
19
+ /** Per-response heuristic results backing the per-interaction feedback chrome. */
20
+ readonly scores: readonly ScoreResult[];
21
+ /** Item outcomes of record from the RP interpreter; empty before submit or without RP. */
22
+ readonly outcomes: Readonly<Record<string, OutcomeValue>>;
23
+ /** This clone's template variables (empty without templateProcessing). */
24
+ readonly templateValues: Readonly<Record<string, OutcomeValue>>;
25
+ /** Completed attempts so far (only ever exceeds 1 for adaptive items). */
26
+ readonly attemptCount: number;
27
+ }
28
+ /**
29
+ * Consumer-supplied input. Optional members deliberately admit explicit
30
+ * `undefined` ("undefined means not provided"), so callers can pass
31
+ * maybe-undefined values straight through; the store reads every member
32
+ * field-wise (`??`/`?.`) and never spread-merges options over defaults.
33
+ */
34
+ export interface AttemptStoreOptions {
35
+ readonly outcomeDeclarations?: readonly OutcomeDeclarationView[] | undefined;
36
+ readonly responseProcessing?: ResponseProcessingView | undefined;
37
+ /** The Response Normalization hook (ADR-0004); applies to scores and outcomes alike. */
38
+ readonly normalization?: ResponseNormalization | undefined;
39
+ readonly templateDeclarations?: readonly TemplateDeclarationView[] | undefined;
40
+ readonly templateProcessing?: TemplateProcessingView | undefined;
41
+ /** Clone seed for template processing; store it to replay the same clone. */
42
+ readonly seed?: number | undefined;
43
+ /** QTI adaptive item: multiple attempts, outcome carry-over, completionStatus lock. */
44
+ readonly adaptive?: boolean | undefined;
45
+ /** Registered vendor `customOperator` implementations by class (opt-in). */
46
+ readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>> | undefined;
47
+ }
48
+ export interface AttemptStore {
49
+ readonly getSnapshot: () => AttemptSnapshot;
50
+ readonly subscribe: (listener: () => void) => () => void;
51
+ readonly setResponse: (responseIdentifier: string, value: ResponseValue) => void;
52
+ /**
53
+ * Imperative interactions (PCI) hold their response internally; a collector pulls it
54
+ * at submit time, before scoring. Returning undefined leaves the response unchanged.
55
+ * Returns the unregister function.
56
+ */
57
+ readonly registerResponseCollector: (responseIdentifier: string, collector: () => ResponseValue | undefined) => () => void;
58
+ readonly submit: () => readonly ScoreResult[];
59
+ readonly reset: () => void;
60
+ }
61
+ export declare function createAttemptStore(declarations: readonly ResponseDeclarationView[], initialResponses: Readonly<Record<string, ResponseValue>>, options?: AttemptStoreOptions): AttemptStore;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The headless Test Controller (ADR-0005): given an assessmentTest view and a seed, it
3
+ * resolves the delivery plan (seeded selection + ordering) once, then answers every
4
+ * navigation, branching, and outcome-processing question as a pure transition over the
5
+ * consumer-persisted session state. It owns no storage and renders nothing.
6
+ */
7
+ import type { AssessmentTestView, TestController } from "./types";
8
+ export interface TestControllerOptions {
9
+ readonly seed: number;
10
+ }
11
+ export declare function createTestController(view: AssessmentTestView, options: TestControllerOptions): TestController;
@@ -0,0 +1,3 @@
1
+ export { createTestController, type TestControllerOptions } from "./controller";
2
+ export { createTestSessionStore, type TestSessionSnapshot, type TestSessionStore, type TestSessionStoreOptions, } from "./session-store";
3
+ export type { AssessmentItemRefView, AssessmentSectionView, AssessmentTestView, BranchRuleView, ItemSessionControlView, OutcomeConditionBranch, OutcomeRuleView, TestController, TestFeedbackView, TestItemResult, TestPartView, TestPlan, TestPlanItem, TestPlanPart, TestSessionState, TimeLimitsView, } from "./types";
@@ -0,0 +1,46 @@
1
+ /**
2
+ * The Test Session Store: glue between the headless Test Controller (ADR-0005) and
3
+ * per-item Attempt Stores. It holds the controller's JSON session state, creates item
4
+ * stores lazily with key-derived seeds (so every clone is replayable from the test
5
+ * seed alone), and feeds item submissions back into the controller so test outcome
6
+ * processing stays current. React-free — UI layers subscribe like any external store;
7
+ * persistence stays with the consumer (store the seed and `snapshot.state`).
8
+ */
9
+ import type { CustomOperatorImplementation, ResponseNormalization } from "../rp";
10
+ import type { AssessmentItemView } from "../runtime";
11
+ import { type AttemptStore } from "../store";
12
+ import type { AssessmentItemRefView, TestController, TestFeedbackView, TestPlanItem, TestSessionState } from "./types";
13
+ export interface TestSessionStoreOptions {
14
+ /**
15
+ * Resolve an item ref to its view. Synchronous by design: load the package's items
16
+ * before mounting the session; return null for refs that cannot be delivered.
17
+ */
18
+ readonly resolveItem: (ref: AssessmentItemRefView) => AssessmentItemView | null;
19
+ /** The test seed: drives plan resolution replay and derives per-item clone seeds. */
20
+ readonly seed: number;
21
+ /** Resume a persisted session (item responses are session-local, not part of it). */
22
+ readonly initialState?: TestSessionState;
23
+ readonly normalization?: ResponseNormalization;
24
+ /** Registered vendor `customOperator` implementations by class (opt-in). */
25
+ readonly customOperators?: Readonly<Record<string, CustomOperatorImplementation>>;
26
+ }
27
+ export interface TestSessionSnapshot {
28
+ readonly state: TestSessionState;
29
+ readonly currentItem: TestPlanItem | null;
30
+ /** The resolved view for the current item, or null when unresolvable. */
31
+ readonly currentItemView: AssessmentItemView | null;
32
+ readonly visibleFeedbacks: readonly TestFeedbackView[];
33
+ }
34
+ export interface TestSessionStore {
35
+ readonly controller: TestController;
36
+ readonly subscribe: (listener: () => void) => () => void;
37
+ readonly getSnapshot: () => TestSessionSnapshot;
38
+ /** The item's Attempt Store — created once per key, reused across navigation. */
39
+ readonly itemStore: (itemKey: string) => AttemptStore | null;
40
+ readonly itemView: (itemKey: string) => AssessmentItemView | null;
41
+ readonly next: () => void;
42
+ readonly canMoveTo: (itemKey: string) => boolean;
43
+ readonly moveTo: (itemKey: string) => void;
44
+ readonly end: () => void;
45
+ }
46
+ export declare function createTestSessionStore(controller: TestController, options: TestSessionStoreOptions): TestSessionStore;