@step-wise/input-exercises 0.1.0

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 (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +245 -0
  3. package/dist/InputExercise/checkInput.d.ts +8 -0
  4. package/dist/InputExercise/checkInput.d.ts.map +1 -0
  5. package/dist/InputExercise/checkInput.js +26 -0
  6. package/dist/InputExercise/getInput.d.ts +24 -0
  7. package/dist/InputExercise/getInput.d.ts.map +1 -0
  8. package/dist/InputExercise/getInput.js +23 -0
  9. package/dist/InputExercise/guards.d.ts +4 -0
  10. package/dist/InputExercise/guards.d.ts.map +1 -0
  11. package/dist/InputExercise/guards.js +30 -0
  12. package/dist/InputExercise/history.d.ts +22 -0
  13. package/dist/InputExercise/history.d.ts.map +1 -0
  14. package/dist/InputExercise/history.js +135 -0
  15. package/dist/InputExercise/index.d.ts +6 -0
  16. package/dist/InputExercise/index.d.ts.map +1 -0
  17. package/dist/InputExercise/index.js +5 -0
  18. package/dist/InputExercise/parameterSerialization.d.ts +5 -0
  19. package/dist/InputExercise/parameterSerialization.d.ts.map +1 -0
  20. package/dist/InputExercise/parameterSerialization.js +15 -0
  21. package/dist/InputExercise/reducerSupport.d.ts +41 -0
  22. package/dist/InputExercise/reducerSupport.d.ts.map +1 -0
  23. package/dist/InputExercise/reducerSupport.js +82 -0
  24. package/dist/InputExercise/solutions.d.ts +7 -0
  25. package/dist/InputExercise/solutions.d.ts.map +1 -0
  26. package/dist/InputExercise/solutions.js +15 -0
  27. package/dist/InputExercise/types.d.ts +70 -0
  28. package/dist/InputExercise/types.d.ts.map +1 -0
  29. package/dist/InputExercise/types.js +1 -0
  30. package/dist/InputExercise/valueOperations.d.ts +4 -0
  31. package/dist/InputExercise/valueOperations.d.ts.map +1 -0
  32. package/dist/InputExercise/valueOperations.js +14 -0
  33. package/dist/MonoExercise/guards.d.ts +3 -0
  34. package/dist/MonoExercise/guards.d.ts.map +1 -0
  35. package/dist/MonoExercise/guards.js +4 -0
  36. package/dist/MonoExercise/index.d.ts +4 -0
  37. package/dist/MonoExercise/index.d.ts.map +1 -0
  38. package/dist/MonoExercise/index.js +3 -0
  39. package/dist/MonoExercise/reducer.d.ts +4 -0
  40. package/dist/MonoExercise/reducer.d.ts.map +1 -0
  41. package/dist/MonoExercise/reducer.js +87 -0
  42. package/dist/MonoExercise/types.d.ts +16 -0
  43. package/dist/MonoExercise/types.d.ts.map +1 -0
  44. package/dist/MonoExercise/types.js +1 -0
  45. package/dist/StepExercise/guards.d.ts +3 -0
  46. package/dist/StepExercise/guards.d.ts.map +1 -0
  47. package/dist/StepExercise/guards.js +4 -0
  48. package/dist/StepExercise/history.d.ts +10 -0
  49. package/dist/StepExercise/history.d.ts.map +1 -0
  50. package/dist/StepExercise/history.js +57 -0
  51. package/dist/StepExercise/index.d.ts +6 -0
  52. package/dist/StepExercise/index.d.ts.map +1 -0
  53. package/dist/StepExercise/index.js +5 -0
  54. package/dist/StepExercise/preprocessing.d.ts +8 -0
  55. package/dist/StepExercise/preprocessing.d.ts.map +1 -0
  56. package/dist/StepExercise/preprocessing.js +27 -0
  57. package/dist/StepExercise/reducer.d.ts +4 -0
  58. package/dist/StepExercise/reducer.d.ts.map +1 -0
  59. package/dist/StepExercise/reducer.js +217 -0
  60. package/dist/StepExercise/types.d.ts +38 -0
  61. package/dist/StepExercise/types.d.ts.map +1 -0
  62. package/dist/StepExercise/types.js +1 -0
  63. package/dist/guards.d.ts +5 -0
  64. package/dist/guards.d.ts.map +1 -0
  65. package/dist/guards.js +5 -0
  66. package/dist/index.d.ts +5 -0
  67. package/dist/index.d.ts.map +1 -0
  68. package/dist/index.js +4 -0
  69. package/dist/tsconfig.tsbuildinfo +1 -0
  70. package/package.json +53 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2020 Step-Wise
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,245 @@
1
+ # @step-wise/input-exercises
2
+
3
+ Build educational exercises that interpret learner input, check it against generated parameters, update practiced skills, and track whether the exercise was solved or given up.
4
+
5
+ This package specializes the reducer model from [@step-wise/exercise-definition](https://www.npmjs.com/package/@step-wise/exercise-definition). Exercise authors describe how to generate a problem and check an answer; the builders in this package supply the solo- and group-reducers.
6
+
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ npm install @step-wise/input-exercises
12
+ ```
13
+
14
+
15
+ ## Mono exercises
16
+
17
+ A `MonoExercise` checks one problem as a whole. The input is either correct or it's not. Create one with `buildMonoExercise`:
18
+
19
+ ```ts
20
+ import { buildMonoExercise, getInput } from '@step-wise/input-exercises'
21
+
22
+ type Parameters = { left: number, right: number }
23
+ type Solution = { answer: number }
24
+
25
+ const addition = buildMonoExercise<Parameters, Solution>({
26
+ metadata: { skill: 'addition' },
27
+ generateParameters: example => example ? { left: 2, right: 3 } : { left: 7, right: 8 },
28
+ getSolution: parameters => ({ answer: parameters.left + parameters.right }),
29
+ checkInput: data => getInput('answer', data, 'number') === data.solution?.answer,
30
+ })
31
+ ```
32
+
33
+ An input exercise specification commonly contains:
34
+
35
+ - `metadata` includes the practiced `skill` or a more involved skill `setup`.
36
+ - `generateParameters(example)` creates the fixed problem parameters. It generally uses randomization and may be synchronous or asynchronous.
37
+ - `getSolution(parameters, inputDependency, staticSolution)` builds the solution. Exercises without input dependencies normally use only `parameters`.
38
+ - `checkInput(data)` decides whether the interpreted learner input is correct. Its data includes the raw and interpreted input, current input dependency, and corresponding solution. It may return either a boolean or `{ correct, report? }`, immediately or through a promise.
39
+
40
+ Only `metadata` and `checkInput` are required. Omitting `generateParameters` uses an empty object.
41
+
42
+ Returning a structured result lets grading retain plain-data details about the transition without putting them in the exercise state:
43
+
44
+ ```ts
45
+ checkInput: data => ({
46
+ correct: getInput('answer', data, 'number') === data.solution?.answer,
47
+ report: { unitCorrect: true },
48
+ })
49
+ ```
50
+
51
+ Existing boolean checks are normalized to `{ correct }` and therefore produce no report. A solo reducer exposes the report directly in its result. A group reducer collects reports by user ID. If `checkInput` omits its report, the reducer result omits it too; an explicit empty object remains an explicit report.
52
+
53
+ `buildMonoExercise` creates both `processSoloAction` and `processGroupActions`. Consumers therefore do not need separate exercise definitions for solo and group use.
54
+
55
+
56
+ ## Step exercises
57
+
58
+ A `StepExercise` first lets the learner answer the complete problem. If the learner gives up, it splits the problem into guided steps:
59
+
60
+ ```ts
61
+ import {buildStepExercise, createStepExerciseMetadata, getInput } from '@step-wise/input-exercises'
62
+
63
+ const multiplication = buildStepExercise({
64
+ metadata: {
65
+ ...createStepExerciseMetadata(['multiply-ones', 'multiply-tens']),
66
+ skill: 'multiplication',
67
+ },
68
+ generateParameters: () => ({ left: 12, right: 3 }),
69
+ getSolution: ({ left, right }) => ({ answer: left * right }),
70
+ checkInput: (data, step) => {
71
+ const answer = getInput('answer', data, 'number')
72
+ switch (step) {
73
+ case 1: return answer === 6
74
+ case 2: return answer === 30
75
+ default: return answer === data.solution?.answer
76
+ }
77
+ },
78
+ })
79
+ ```
80
+
81
+ Steps are numbered from `1`. The unsplit main problem uses step `0`. `getCurrentStep(state)` returns this number.
82
+
83
+ `createStepExerciseMetadata(steps)` stores the step structure and combines its defined skills into a skill setup. A step may be `undefined` when it does not have a separately tracked skill.
84
+
85
+
86
+ ## Substeps
87
+
88
+ A step can contain an array of substeps:
89
+
90
+ ```ts
91
+ const metadata = createStepExerciseMetadata(['expand-brackets', ['combine-like-terms', 'simplify-coefficients']])
92
+ ```
93
+
94
+ A substep array must contain at least two entries; otherwise it should be an ordinary step. Substeps are numbered from `1`. For the main problem and ordinary steps, `substep` is `0`:
95
+
96
+ ```ts
97
+ checkInput: (data, step, substep) => {
98
+ if (step === 0) return checkCompleteProblem(data)
99
+ if (step === 1) return checkExpansion(data)
100
+ if (substep === 1) return checkLikeTerms(data)
101
+ return checkCoefficients(data)
102
+ }
103
+ ```
104
+
105
+ One input may solve one or more substeps if `checkInput` accepts that same input for them. Attempts are stored per step, not separately for every substep.
106
+
107
+
108
+ ## Custom value types
109
+
110
+ An exercise can opt into domain-specific behavior through its optional `valueTypes` registry. Each value type may provide input interpretation, parameter serialization, equality, or any combination of those capabilities.
111
+
112
+ ```ts
113
+ const exercise = buildMonoExercise({
114
+ metadata: { skill: 'algebra' },
115
+ valueTypes: mathematicsValueTypes,
116
+ generateParameters: () => ({ expression: createExpression() }),
117
+ checkInput: data => compareInputs('answer', data),
118
+ })
119
+ ```
120
+
121
+ The builders combine the supplied registry with the fundamental Integer and MultipleChoice value types, validate it, and capture its adapters privately. The built exercise exposes `valueOperations` with `serialize`, `deserialize`, `interpretInput`, `toInputValue`, and `areValuesEqual`; consumers never need the registry or its adapters. Generated parameters and submitted actions use the same captured operations internally. An omitted registry therefore still provides Integer and MultipleChoice interpretation and equality without exercise-level configuration.
122
+
123
+ Use `combineValueTypes` from [@step-wise/value-types](https://www.npmjs.com/package/@step-wise/value-types) when an exercise needs more than one domain. Duplicate type names and incomplete adapters throw instead of being silently overwritten.
124
+
125
+
126
+ ## Raw and interpreted input
127
+
128
+ Input actions contain serializable raw input values. Before `checkInput` runs, the package interprets those values into their domain values. Its argument contains both forms:
129
+
130
+ ```ts
131
+ checkInput: ({ rawInput, input, parameters, solution, metadata }) => {
132
+ // rawInput: values suitable for storage and transport
133
+ // input: interpreted numbers, quantities, expressions, and other domain values
134
+ return input.answer === solution?.answer
135
+ }
136
+ ```
137
+
138
+ Because every exercise defines different fields, interpreted input is initially typed as a record of unknown values. `getInput` retrieves one field, checks its runtime type, and returns the corresponding TypeScript type:
139
+
140
+ ```ts
141
+ const count = getInput('count', data, 'number')
142
+ const name = getInput('name', data, 'string')
143
+ const quantity = getInput('quantity', data, Quantity)
144
+ ```
145
+
146
+ Use `getInputs` when several fields share a type or need a matching list of types:
147
+
148
+ ```ts
149
+ const [left, right] = getInputs(['left', 'right'], data, 'number')
150
+ const [count, quantity] = getInputs(['count', 'quantity'], data, ['number', Quantity])
151
+ ```
152
+
153
+ Both helpers throw when a field is missing or has an unexpected type. Interpretation itself is provided by [@step-wise/input-interpretation](https://www.npmjs.com/package/@step-wise/input-interpretation).
154
+
155
+
156
+ ## Defining a solution
157
+
158
+ `getSolution` is optional. If it is absent, `data.solution` is `undefined`, and `checkInput` can compare the interpreted input directly with the parameters:
159
+
160
+ ```ts
161
+ checkInput: ({ input, parameters }) => input.answer === parameters.left + parameters.right
162
+ ```
163
+
164
+ Most exercises derive their complete solution directly from the parameters:
165
+
166
+ ```ts
167
+ getSolution: parameters => ({
168
+ answer: parameters.left + parameters.right,
169
+ })
170
+ ```
171
+
172
+ The framework always calls `getSolution(parameters, inputDependency, staticSolution)`. Ordinary exercises can omit unused arguments. This keeps simple definitions short while giving input-dependent exercises access to the complete lifecycle.
173
+
174
+
175
+ ## Solutions that depend on earlier input
176
+
177
+ Sometimes the appropriate solution depends on how the learner approached the problem. Such exercises can maintain an input dependency in their state:
178
+
179
+ ```ts
180
+ updateInputDependency: ({ previousInputDependency, input }) =>
181
+ input.solveFor === undefined ? previousInputDependency : input.solveFor as 'left' | 'right',
182
+
183
+ getStaticSolution: parameters => ({
184
+ total: parameters.left + parameters.right,
185
+ }),
186
+
187
+ getSolution: (parameters, solveFor, staticSolution) => {
188
+ if (solveFor === 'left') return { left: staticSolution.total! - parameters.right }
189
+ return { right: staticSolution.total! - parameters.left }
190
+ },
191
+ ```
192
+
193
+ The lifecycle consists of three optional callbacks. The input dependency initially is `undefined`:
194
+
195
+ - `getStaticSolution(parameters)` calculates a reusable, input-independent partial solution.
196
+ - `updateInputDependency({ parameters, previousInputDependency, staticSolution, input, step })` updates the dependency from the input submitted for the current step. The unsplit main problem uses step `0`.
197
+ - `getSolution(parameters, inputDependency, staticSolution)` calculates the dynamic portion of the solution. The framework merges this over the static portion.
198
+
199
+ All three callbacks may be synchronous or asynchronous. The runtime definition checks enforce these relationships:
200
+
201
+ - `getStaticSolution` requires `updateInputDependency`.
202
+ - `updateInputDependency` requires `getSolution`.
203
+
204
+ If no updater exists, the resolution helper preserves the previous dependency. Returning `undefined` removes the dependency from state. If no static generator exists, the package supplies `{}` as the static solution. Static fields are automatically included in the final solution; dynamic fields with the same names override them. The builder stores dependencies through the exercise's generic serialization operation, so they may contain registered domain values while persisted exercise state remains plain data. Group mode stores a separate dependency for each participant. An input action may include `adoptUserHistory`, identifying the participant whose work it continues. The reducer then starts from that participant's previous input dependency and stores the updated dependency for the submitting participant. Accumulated input follows these references backward, so chained branches are reconstructed without copying earlier actions.
205
+
206
+ The package exports focused helpers for consumers implementing the lifecycle:
207
+
208
+ - `resolveUpdatedInputDependency(definition, data)`
209
+ - `resolveStaticSolution(definition, parameters)`
210
+ - `resolveSolution(definition, parameters, inputDependency, staticSolution)`
211
+
212
+
213
+ ## Looking up previous input
214
+
215
+ The history helpers accept either solo or group exercise instances:
216
+
217
+ - `getLastRawInput(instance, userId?, options?)` returns stored input values.
218
+ - `getLastInput(exercise, instance, userId?, options?)` returns interpreted values using the exercise's value types.
219
+ - `getAccumulatedRawInput(instance, userId?, options?)` combines partial input actions, with later values replacing earlier values for repeated fields.
220
+ - `getAccumulatedInput(exercise, instance, userId?, options?)` provides the interpreted version of that combined input.
221
+ - `getAccumulatedReport(instance, userId?, options?)` combines reports from the corresponding resolved input actions in the same way.
222
+ - `hasPreviousInput(instance, userId?)` reports whether an input exists.
223
+ - `getLastRawInputAtStep`, `getLastInputAtStep(exercise, instance, step, userId?, options?)`, and `hasPreviousInputAtStep` provide the corresponding operations for one step.
224
+
225
+ For group histories, `userId` is required. By default, lookups may return input from a pending group event. Pass `{ resolvedOnly: true }` to ignore pending actions. The accumulated helpers also accept `throughEventIndex` to reconstruct input at an earlier point in the history:
226
+
227
+ ```ts
228
+ const input = getAccumulatedInput(exercise, instance, userId, { resolvedOnly: true, throughEventIndex: 4 })
229
+ ```
230
+
231
+
232
+ ## TypeScript types
233
+
234
+ The main author-facing types are:
235
+
236
+ - `MonoExerciseSpec` and `MonoExercise` for single-stage exercises.
237
+ - `StepExerciseSpec` and `StepExercise` for guided exercises.
238
+ - `InputExerciseParameters`, `InputExerciseInput`, and `InputExerciseSolution` for exercise-specific data.
239
+ - `CheckInputData` for the object supplied to `checkInput`, including the exercise-bound `areValuesEqual` operation.
240
+ - `ValueTypes` for optional domain capabilities on an exercise specification, and `InputExerciseValueOperations` for the operations exposed by a built exercise.
241
+ - `GetSolution`, `GetStaticSolution`, and `UpdateInputDependency` for solution generation.
242
+ - `StepExerciseSteps`, `StepExerciseState`, and `StepExerciseMetadata` for step structures.
243
+ - `InputExerciseAction` and `InputExerciseRawInput` for stored learner actions.
244
+
245
+ Prefer supplying concrete parameter and solution types to the builders. This gives `generateParameters`, the solution callbacks, and `checkInput` a shared inferred contract.
@@ -0,0 +1,8 @@
1
+ import type { CheckInputResult, GroupInputExerciseReport, InputExerciseReport } from './types.ts';
2
+ export declare function normalizeCheckInputResult(result: CheckInputResult): {
3
+ correct: boolean;
4
+ report?: InputExerciseReport;
5
+ };
6
+ export declare function getGroupInputExerciseReport(userIds: readonly (string | undefined)[], reports: readonly (InputExerciseReport | undefined)[]): GroupInputExerciseReport | undefined;
7
+ export declare function mergeInputExerciseReports(previousReport: InputExerciseReport | undefined, report: InputExerciseReport | undefined): InputExerciseReport | undefined;
8
+ //# sourceMappingURL=checkInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkInput.d.ts","sourceRoot":"","sources":["../../src/InputExercise/checkInput.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAEjG,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,gBAAgB,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,mBAAmB,CAAA;CAAE,CAKtH;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,SAAS,CAAC,MAAM,GAAG,SAAS,CAAC,EAAE,EAAE,OAAO,EAAE,SAAS,CAAC,mBAAmB,GAAG,SAAS,CAAC,EAAE,GAAG,wBAAwB,GAAG,SAAS,CAQjL;AAED,wBAAgB,yBAAyB,CAAC,cAAc,EAAE,mBAAmB,GAAG,SAAS,EAAE,MAAM,EAAE,mBAAmB,GAAG,SAAS,GAAG,mBAAmB,GAAG,SAAS,CAGnK"}
@@ -0,0 +1,26 @@
1
+ import { hasOnlyKeys, isBoolean, isPlainDataObject, isPlainObject } from '@step-wise/js-utils';
2
+ export function normalizeCheckInputResult(result) {
3
+ if (isBoolean(result))
4
+ return { correct: result };
5
+ if (!isPlainObject(result) || !hasOnlyKeys(result, ['correct', 'report']) || !isBoolean(result.correct))
6
+ throw new TypeError(`Invalid checkInput result: expected a boolean or an object with a boolean "correct" property and an optional report.`);
7
+ if (result.report !== undefined && !isPlainDataObject(result.report))
8
+ throw new TypeError(`Invalid checkInput report: expected a plain data object.`);
9
+ return result;
10
+ }
11
+ export function getGroupInputExerciseReport(userIds, reports) {
12
+ const report = Object.fromEntries(reports.flatMap((userReport, index) => {
13
+ if (userReport === undefined)
14
+ return [];
15
+ const userId = userIds[index];
16
+ if (userId === undefined)
17
+ throw new TypeError(`A userId is required when creating a group exercise report.`);
18
+ return [[userId, userReport]];
19
+ }));
20
+ return Object.keys(report).length === 0 ? undefined : report;
21
+ }
22
+ export function mergeInputExerciseReports(previousReport, report) {
23
+ if (report === undefined)
24
+ return previousReport;
25
+ return { ...previousReport, ...report };
26
+ }
@@ -0,0 +1,24 @@
1
+ import type { CheckInputData } from './types.ts';
2
+ type PrimitiveInputType = 'number' | 'string' | 'boolean';
3
+ type PrimitiveInputTypeMap = {
4
+ number: number;
5
+ string: string;
6
+ boolean: boolean;
7
+ };
8
+ type InputConstructor = abstract new (...args: never[]) => unknown;
9
+ type InputType = InputConstructor | PrimitiveInputType;
10
+ type InputTypesForKeys<Keys extends readonly string[]> = {
11
+ readonly [Index in keyof Keys]: InputType;
12
+ };
13
+ type InputInstance<Type extends InputType> = Type extends PrimitiveInputType ? PrimitiveInputTypeMap[Type] : Type extends InputConstructor ? InstanceType<Type> : never;
14
+ type InputInstances<Types extends readonly InputType[]> = {
15
+ [Index in keyof Types]: InputInstance<Types[Index]>;
16
+ };
17
+ export declare function getInput<Type extends PrimitiveInputType>(key: string, data: CheckInputData, inputType: Type): PrimitiveInputTypeMap[Type];
18
+ export declare function getInput<Constructor extends InputConstructor>(key: string, data: CheckInputData, inputType: Constructor): InstanceType<Constructor>;
19
+ export declare function getInputs<const Keys extends readonly string[], Type extends InputType>(keys: Keys, data: CheckInputData, inputType: Type): {
20
+ [Index in keyof Keys]: InputInstance<Type>;
21
+ };
22
+ export declare function getInputs<const Keys extends readonly string[], const Types extends InputTypesForKeys<Keys>>(keys: Keys, data: CheckInputData, inputTypes: Types): InputInstances<Types>;
23
+ export {};
24
+ //# sourceMappingURL=getInput.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getInput.d.ts","sourceRoot":"","sources":["../../src/InputExercise/getInput.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhD,KAAK,kBAAkB,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAA;AACzD,KAAK,qBAAqB,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAA;AAEjF,KAAK,gBAAgB,GAAG,QAAQ,MAAM,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAA;AAClE,KAAK,SAAS,GAAG,gBAAgB,GAAG,kBAAkB,CAAA;AAEtD,KAAK,iBAAiB,CAAC,IAAI,SAAS,SAAS,MAAM,EAAE,IAAI;IAAE,QAAQ,EAAE,KAAK,IAAI,MAAM,IAAI,GAAG,SAAS;CAAE,CAAA;AACtG,KAAK,aAAa,CAAC,IAAI,SAAS,SAAS,IAAI,IAAI,SAAS,kBAAkB,GAAG,qBAAqB,CAAC,IAAI,CAAC,GAAG,IAAI,SAAS,gBAAgB,GAAG,YAAY,CAAC,IAAI,CAAC,GAAG,KAAK,CAAA;AACvK,KAAK,cAAc,CAAC,KAAK,SAAS,SAAS,SAAS,EAAE,IAAI;KAAG,KAAK,IAAI,MAAM,KAAK,GAAG,aAAa,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;CAAE,CAAA;AAkBjH,wBAAgB,QAAQ,CAAC,IAAI,SAAS,kBAAkB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,SAAS,EAAE,IAAI,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAA;AAC1I,wBAAgB,QAAQ,CAAC,WAAW,SAAS,gBAAgB,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,EAAE,SAAS,EAAE,WAAW,GAAG,YAAY,CAAC,WAAW,CAAC,CAAA;AAMpJ,wBAAgB,SAAS,CAAC,KAAK,CAAC,IAAI,SAAS,SAAS,MAAM,EAAE,EAAE,IAAI,SAAS,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,SAAS,EAAE,IAAI,GAAG;KAAG,KAAK,IAAI,MAAM,IAAI,GAAG,aAAa,CAAC,IAAI,CAAC;CAAE,CAAA;AAC1L,wBAAgB,SAAS,CAAC,KAAK,CAAC,IAAI,SAAS,SAAS,MAAM,EAAE,EAAE,KAAK,CAAC,KAAK,SAAS,iBAAiB,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,EAAE,UAAU,EAAE,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,CAAA"}
@@ -0,0 +1,23 @@
1
+ function isInputConstructor(inputType) {
2
+ return typeof inputType === 'function';
3
+ }
4
+ function isInputTypeArray(inputTypes) {
5
+ return Array.isArray(inputTypes);
6
+ }
7
+ function getInputValue(key, data, inputType) {
8
+ const input = data.input[key];
9
+ const matches = isInputConstructor(inputType) ? input instanceof inputType : typeof input === inputType;
10
+ if (!matches)
11
+ throw new TypeError(`Invalid getInput call: input "${key}" has an unexpected type.`);
12
+ return input;
13
+ }
14
+ export function getInput(key, data, inputType) {
15
+ return getInputValue(key, data, inputType);
16
+ }
17
+ export function getInputs(keys, data, inputTypes) {
18
+ if (!isInputTypeArray(inputTypes))
19
+ return keys.map(key => getInputValue(key, data, inputTypes));
20
+ if (keys.length !== inputTypes.length)
21
+ throw new RangeError(`Invalid getInputs call: received ${keys.length} keys and ${inputTypes.length} input types.`);
22
+ return keys.map((key, index) => getInputValue(key, data, inputTypes[index]));
23
+ }
@@ -0,0 +1,4 @@
1
+ export declare function hasInputExerciseProperties(value: unknown): value is Record<string, unknown> & {
2
+ metadata: Record<string, unknown>;
3
+ };
4
+ //# sourceMappingURL=guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guards.d.ts","sourceRoot":"","sources":["../../src/InputExercise/guards.ts"],"names":[],"mappings":"AAMA,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAUnI"}
@@ -0,0 +1,30 @@
1
+ import { isPlainObject } from '@step-wise/js-utils';
2
+ import { isExercise } from '@step-wise/exercise-definition';
3
+ // Check if an exercise has the properties expected of an input exercise.
4
+ export function hasInputExerciseProperties(value) {
5
+ if (!isPlainObject(value) || !isExercise(value))
6
+ return false;
7
+ if (!isInputExerciseValueOperations(value.valueOperations))
8
+ return false;
9
+ if (typeof value.checkInput !== 'function' || typeof value.processSoloAction !== 'function' || typeof value.processGroupActions !== 'function')
10
+ return false;
11
+ if (value.updateInputDependency !== undefined && typeof value.updateInputDependency !== 'function')
12
+ return false;
13
+ if (value.getStaticSolution !== undefined && typeof value.getStaticSolution !== 'function')
14
+ return false;
15
+ if (value.getSolution !== undefined && typeof value.getSolution !== 'function')
16
+ return false;
17
+ if (value.getStaticSolution !== undefined && value.updateInputDependency === undefined)
18
+ return false;
19
+ if (value.updateInputDependency !== undefined && value.getSolution === undefined)
20
+ return false;
21
+ return true;
22
+ }
23
+ function isInputExerciseValueOperations(value) {
24
+ return isPlainObject(value)
25
+ && typeof value.serialize === 'function'
26
+ && typeof value.deserialize === 'function'
27
+ && typeof value.interpretInput === 'function'
28
+ && typeof value.toInputValue === 'function'
29
+ && typeof value.areValuesEqual === 'function';
30
+ }
@@ -0,0 +1,22 @@
1
+ import { type BaseExerciseInstanceByMode, type ExerciseMode, type ExerciseState } from '@step-wise/exercise-definition';
2
+ import type { GroupInputExerciseReport, InputExerciseAction, InputExerciseInput, InputExerciseRawInput, InputExerciseReport, InputExerciseValueOperations, SoloInputExerciseReport } from './types.ts';
3
+ export type InputExerciseHistoryData<TState extends ExerciseState = ExerciseState> = {
4
+ [Mode in ExerciseMode]: Pick<BaseExerciseInstanceByMode<InputExerciseAction, TState, Record<string, never>, Mode extends 'solo' ? SoloInputExerciseReport : GroupInputExerciseReport>[Mode], 'mode' | 'initialState' | 'history'>;
5
+ }[ExerciseMode];
6
+ export type LastInputOptions = {
7
+ resolvedOnly?: boolean;
8
+ };
9
+ export type AccumulatedInputOptions = LastInputOptions & {
10
+ throughEventIndex?: number;
11
+ };
12
+ export declare function getLastRawInput(instance: InputExerciseHistoryData, userId?: string, options?: LastInputOptions): InputExerciseRawInput | undefined;
13
+ export declare function getAccumulatedRawInput(instance: InputExerciseHistoryData, userId?: string, options?: AccumulatedInputOptions): InputExerciseRawInput | undefined;
14
+ export declare function getAccumulatedReport(instance: InputExerciseHistoryData, userId?: string, options?: Pick<AccumulatedInputOptions, 'throughEventIndex'>): InputExerciseReport | undefined;
15
+ export declare function getAccumulatedInput(exercise: {
16
+ valueOperations: InputExerciseValueOperations;
17
+ }, instance: InputExerciseHistoryData, userId?: string, options?: AccumulatedInputOptions): InputExerciseInput | undefined;
18
+ export declare function getLastInput(exercise: {
19
+ valueOperations: InputExerciseValueOperations;
20
+ }, instance: InputExerciseHistoryData, userId?: string, options?: LastInputOptions): InputExerciseInput | undefined;
21
+ export declare function hasPreviousInput(instance: InputExerciseHistoryData, userId?: string): boolean;
22
+ //# sourceMappingURL=history.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../../src/InputExercise/history.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,0BAA0B,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAgC,MAAM,gCAAgC,CAAA;AAErJ,OAAO,KAAK,EAAE,wBAAwB,EAAE,mBAAmB,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAA;AAGtM,MAAM,MAAM,wBAAwB,CAAC,MAAM,SAAS,aAAa,GAAG,aAAa,IAAI;KACnF,IAAI,IAAI,YAAY,GAAG,IAAI,CAAC,0BAA0B,CACtD,mBAAmB,EACnB,MAAM,EACN,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EACrB,IAAI,SAAS,MAAM,GAAG,uBAAuB,GAAG,wBAAwB,CACxE,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS,CAAC;CAC7C,CAAC,YAAY,CAAC,CAAA;AAEf,MAAM,MAAM,gBAAgB,GAAG;IAC9B,YAAY,CAAC,EAAE,OAAO,CAAA;CACtB,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG,gBAAgB,GAAG;IACxD,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC1B,CAAA;AAGD,wBAAgB,eAAe,CAAC,QAAQ,EAAE,wBAAwB,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,qBAAqB,GAAG,SAAS,CAyBtJ;AAGD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,wBAAwB,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE,uBAA4B,GAAG,qBAAqB,GAAG,SAAS,CA0CpK;AAGD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,wBAAwB,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE,IAAI,CAAC,uBAAuB,EAAE,mBAAmB,CAAM,GAAG,mBAAmB,GAAG,SAAS,CA2C3L;AAGD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE;IAAE,eAAe,EAAE,4BAA4B,CAAA;CAAE,EAAE,QAAQ,EAAE,wBAAwB,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE,uBAA4B,GAAG,kBAAkB,GAAG,SAAS,CAG3N;AAGD,wBAAgB,YAAY,CAAC,QAAQ,EAAE;IAAE,eAAe,EAAE,4BAA4B,CAAA;CAAE,EAAE,QAAQ,EAAE,wBAAwB,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAqB,GAAG,kBAAkB,GAAG,SAAS,CAG7M;AAGD,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,wBAAwB,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAE7F"}
@@ -0,0 +1,135 @@
1
+ import { throwUnsupportedExerciseMode } from '@step-wise/exercise-definition';
2
+ // Get the last given raw input from the user. For group exercises, this may be an unresolved action input unless resolvedOnly is true.
3
+ export function getLastRawInput(instance, userId, options = {}) {
4
+ const { mode } = instance;
5
+ const { resolvedOnly = false } = options;
6
+ switch (mode) {
7
+ case 'solo':
8
+ for (let index = instance.history.length - 1; index >= 0; index--) {
9
+ const action = instance.history[index].action;
10
+ if (action.type === 'input')
11
+ return action.input;
12
+ }
13
+ return undefined;
14
+ case 'group': {
15
+ if (userId === undefined)
16
+ throw new TypeError(`A userId is required when retrieving input from a group exercise history.`);
17
+ let historyUserId = userId;
18
+ for (let index = instance.history.length - 1; index >= 0; index--) {
19
+ const event = instance.history[index];
20
+ const action = event.actions.find(userAction => userAction.userId === historyUserId)?.action;
21
+ if (action?.type !== 'input')
22
+ continue;
23
+ if (!resolvedOnly || 'state' in event)
24
+ return action.input;
25
+ historyUserId = action.adoptUserHistory ?? historyUserId;
26
+ }
27
+ return undefined;
28
+ }
29
+ default:
30
+ return throwUnsupportedExerciseMode(mode);
31
+ }
32
+ }
33
+ // Combine a user's input actions through the requested history event. Later values overwrite earlier values with the same field ID.
34
+ export function getAccumulatedRawInput(instance, userId, options = {}) {
35
+ const { mode } = instance;
36
+ const { resolvedOnly = false, throughEventIndex = instance.history.length - 1 } = options;
37
+ const lastIndex = Math.min(throughEventIndex, instance.history.length - 1);
38
+ // Set up accumulators.
39
+ const input = {};
40
+ let hasInput = false;
41
+ const addInput = (action) => {
42
+ if (action?.type !== 'input')
43
+ return;
44
+ Object.assign(input, action.input);
45
+ hasInput = true;
46
+ };
47
+ // Depending on the mode, walk through the actions and add respective inputs.
48
+ switch (mode) {
49
+ case 'solo':
50
+ for (let index = 0; index <= lastIndex; index++)
51
+ addInput(instance.history[index].action);
52
+ break;
53
+ case 'group':
54
+ if (userId === undefined)
55
+ throw new TypeError(`A userId is required when retrieving input from a group exercise history.`);
56
+ let historyUserId = userId;
57
+ for (let index = lastIndex; index >= 0; index--) {
58
+ const event = instance.history[index];
59
+ const action = event.actions.find(userAction => userAction.userId === historyUserId)?.action;
60
+ if (action?.type !== 'input')
61
+ continue;
62
+ if (!resolvedOnly || 'state' in event) {
63
+ Object.entries(action.input).forEach(([id, value]) => {
64
+ if (!(id in input))
65
+ input[id] = value;
66
+ });
67
+ hasInput = true;
68
+ }
69
+ historyUserId = action.adoptUserHistory ?? historyUserId;
70
+ }
71
+ break;
72
+ default:
73
+ return throwUnsupportedExerciseMode(mode);
74
+ }
75
+ return hasInput ? input : undefined;
76
+ }
77
+ // Combine reports belonging to a user's resolved input actions. Later values overwrite earlier values with the same key.
78
+ export function getAccumulatedReport(instance, userId, options = {}) {
79
+ const { mode } = instance;
80
+ const { throughEventIndex = instance.history.length - 1 } = options;
81
+ const lastIndex = Math.min(throughEventIndex, instance.history.length - 1);
82
+ const report = {};
83
+ let hasReport = false;
84
+ switch (mode) {
85
+ case 'solo':
86
+ for (let index = 0; index <= lastIndex; index++) {
87
+ const event = instance.history[index];
88
+ if (event.action.type !== 'input' || event.report === undefined)
89
+ continue;
90
+ Object.assign(report, event.report);
91
+ hasReport = true;
92
+ }
93
+ break;
94
+ case 'group': {
95
+ if (userId === undefined)
96
+ throw new TypeError(`A userId is required when retrieving a report from a group exercise history.`);
97
+ let historyUserId = userId;
98
+ for (let index = lastIndex; index >= 0; index--) {
99
+ const event = instance.history[index];
100
+ const action = event.actions.find(userAction => userAction.userId === historyUserId)?.action;
101
+ if (action?.type !== 'input')
102
+ continue;
103
+ if ('state' in event) {
104
+ const eventReport = event.report?.[historyUserId];
105
+ if (eventReport !== undefined) {
106
+ Object.entries(eventReport).forEach(([key, value]) => {
107
+ if (!(key in report))
108
+ report[key] = value;
109
+ });
110
+ hasReport = true;
111
+ }
112
+ }
113
+ historyUserId = action.adoptUserHistory ?? historyUserId;
114
+ }
115
+ break;
116
+ }
117
+ default:
118
+ return throwUnsupportedExerciseMode(mode);
119
+ }
120
+ return hasReport ? report : undefined;
121
+ }
122
+ // Combine and interpret a user's input actions through the requested history event.
123
+ export function getAccumulatedInput(exercise, instance, userId, options = {}) {
124
+ const rawInput = getAccumulatedRawInput(instance, userId, options);
125
+ return rawInput === undefined ? undefined : exercise.valueOperations.interpretInput(rawInput);
126
+ }
127
+ // Get the last given input from the user and interpret all its values.
128
+ export function getLastInput(exercise, instance, userId, options = {}) {
129
+ const rawInput = getLastRawInput(instance, userId, options);
130
+ return rawInput === undefined ? undefined : exercise.valueOperations.interpretInput(rawInput);
131
+ }
132
+ // Check if a user has made a previous input at the given step.
133
+ export function hasPreviousInput(instance, userId) {
134
+ return getLastRawInput(instance, userId) !== undefined;
135
+ }
@@ -0,0 +1,6 @@
1
+ export * from './types.ts';
2
+ export { type GroupInputExerciseAttemptState, type GroupInputExerciseDependencyState, type InputExerciseAttemptState, type InputExerciseDependencyState, type InputExerciseReducerInput, type SoloInputExerciseAttemptState, type SoloInputExerciseDependencyState, getInputDependency } from './reducerSupport.ts';
3
+ export * from './getInput.ts';
4
+ export * from './history.ts';
5
+ export * from './solutions.ts';
6
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/InputExercise/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,KAAK,8BAA8B,EAAE,KAAK,iCAAiC,EAAE,KAAK,yBAAyB,EAAE,KAAK,4BAA4B,EAAE,KAAK,yBAAyB,EAAE,KAAK,6BAA6B,EAAE,KAAK,gCAAgC,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AACnT,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,gBAAgB,CAAA"}
@@ -0,0 +1,5 @@
1
+ export * from './types.js';
2
+ export { getInputDependency } from './reducerSupport.js';
3
+ export * from './getInput.js';
4
+ export * from './history.js';
5
+ export * from './solutions.js';
@@ -0,0 +1,5 @@
1
+ import { type PlainDataObject } from '@step-wise/js-utils';
2
+ import type { InputExerciseParameters, InputExerciseValueOperations } from './types.ts';
3
+ export declare function serializeInputExerciseParameters(parameters: InputExerciseParameters, serialize: InputExerciseValueOperations['serialize']): PlainDataObject;
4
+ export declare function deserializeInputExerciseParameters<TParameters extends InputExerciseParameters>(parameters: PlainDataObject, deserialize: InputExerciseValueOperations['deserialize']): TParameters;
5
+ //# sourceMappingURL=parameterSerialization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parameterSerialization.d.ts","sourceRoot":"","sources":["../../src/InputExercise/parameterSerialization.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,eAAe,EAAqB,MAAM,qBAAqB,CAAA;AAE7E,OAAO,KAAK,EAAE,uBAAuB,EAAE,4BAA4B,EAAE,MAAM,YAAY,CAAA;AAGvF,wBAAgB,gCAAgC,CAAC,UAAU,EAAE,uBAAuB,EAAE,SAAS,EAAE,4BAA4B,CAAC,WAAW,CAAC,GAAG,eAAe,CAI3J;AAGD,wBAAgB,kCAAkC,CAAC,WAAW,SAAS,uBAAuB,EAAE,UAAU,EAAE,eAAe,EAAE,WAAW,EAAE,4BAA4B,CAAC,aAAa,CAAC,GAAG,WAAW,CAIlM"}
@@ -0,0 +1,15 @@
1
+ import { isPlainDataObject } from '@step-wise/js-utils';
2
+ // Serialize runtime parameters and ensure that the result is suitable for storage.
3
+ export function serializeInputExerciseParameters(parameters, serialize) {
4
+ const serializedParameters = serialize(parameters);
5
+ if (!isPlainDataObject(serializedParameters))
6
+ throw new TypeError('Invalid generated input-exercise parameters: serialization must result in a plain data object.');
7
+ return serializedParameters;
8
+ }
9
+ // Restore stored parameters before passing them to author-facing input-exercise logic.
10
+ export function deserializeInputExerciseParameters(parameters, deserialize) {
11
+ const deserializedParameters = deserialize(parameters);
12
+ if (typeof deserializedParameters !== 'object' || deserializedParameters === null || Array.isArray(deserializedParameters))
13
+ throw new TypeError('Invalid stored input-exercise parameters: deserialization must result in an object.');
14
+ return deserializedParameters;
15
+ }