@lxpack/validators 0.1.1 → 0.2.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.
- package/README.md +35 -13
- package/dist/index.d.ts +196 -17
- package/dist/index.js +300 -47
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/eddiethedean/lxpack/blob/main/LICENSE)
|
|
6
6
|
[](https://nodejs.org/)
|
|
7
7
|
|
|
8
|
-
Zod schemas and filesystem validation for LXPack course manifests.
|
|
8
|
+
Zod schemas and filesystem validation for LXPack course manifests — including flow, variables, and component lessons (v0.2.0).
|
|
9
9
|
|
|
10
10
|
Part of [LXPack](https://github.com/eddiethedean/lxpack) — an AI-native learning experience compiler and runtime.
|
|
11
11
|
|
|
@@ -14,6 +14,7 @@ Part of [LXPack](https://github.com/eddiethedean/lxpack) — an AI-native learni
|
|
|
14
14
|
| CLI | [`@lxpack/cli`](../cli/README.md) |
|
|
15
15
|
| Packaging | [`@lxpack/scorm`](../scorm/README.md) |
|
|
16
16
|
| Runtime | [`@lxpack/runtime`](../runtime/README.md) |
|
|
17
|
+
| Components | [`@lxpack/components`](../components/README.md) |
|
|
17
18
|
|
|
18
19
|
## Install
|
|
19
20
|
|
|
@@ -44,7 +45,9 @@ if (!result.valid) {
|
|
|
44
45
|
const { manifest } = result;
|
|
45
46
|
const bundle = await buildRuntimeAssessmentBundle("/path/to/my-course", manifest);
|
|
46
47
|
// bundle.assessments — learner-facing questions (no correct flags)
|
|
47
|
-
// bundle.answerKeys — scoring keys for the runtime
|
|
48
|
+
// bundle.answerKeys — scoring keys for the runtime
|
|
49
|
+
// bundle.configs — maxAttempts, shuffleChoices, showFeedback per assessment
|
|
50
|
+
// bundle.feedback — questionId → explanation text for feedback modes
|
|
48
51
|
}
|
|
49
52
|
```
|
|
50
53
|
|
|
@@ -65,6 +68,14 @@ if (Array.isArray(loaded)) {
|
|
|
65
68
|
const parsed = courseManifestSchema.safeParse(manifestObject);
|
|
66
69
|
```
|
|
67
70
|
|
|
71
|
+
### Flow validation
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { validateFlow, detectFlowCycles, collectActivityIds } from "@lxpack/validators";
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`validateCourse` runs flow checks when `manifest.flow` is present: valid `goto` targets, known condition shapes, and cycle detection.
|
|
78
|
+
|
|
68
79
|
### Safe path resolution
|
|
69
80
|
|
|
70
81
|
```ts
|
|
@@ -78,34 +89,43 @@ isPathContained(courseDir, abs); // true if inside course root
|
|
|
78
89
|
|
|
79
90
|
| Export | Description |
|
|
80
91
|
|--------|-------------|
|
|
81
|
-
| `validateCourse(dir)` | Parse `course.yaml`, validate schema,
|
|
92
|
+
| `validateCourse(dir)` | Parse `course.yaml`, validate schema, flow, files, symlink containment |
|
|
82
93
|
| `loadManifest(courseDir)` | Load and parse `course.yaml` |
|
|
83
|
-
| `buildRuntimeAssessmentBundle(dir, manifest)` | Load assessments; split learner view
|
|
84
|
-
| `toLearnerAssessment(assessment)` | Strip `correct`
|
|
94
|
+
| `buildRuntimeAssessmentBundle(dir, manifest)` | Load assessments; split learner view, keys, configs, feedback |
|
|
95
|
+
| `toLearnerAssessment(assessment)` | Strip `correct` from choices; extract config and feedback maps |
|
|
96
|
+
| `validateFlow(manifest)` | Flow rule and target validation |
|
|
97
|
+
| `detectFlowCycles(flow)` | Cycle detection for branching graphs |
|
|
98
|
+
| `collectActivityIds(manifest)` | Lesson and assessment IDs for flow targets |
|
|
99
|
+
| `conditionSchema`, `flowRuleSchema` | Zod schemas for flow conditions and rules |
|
|
100
|
+
| `BUILTIN_COMPONENT_IDS`, `isBuiltinComponentId` | Allowed built-in component lesson IDs |
|
|
85
101
|
| `resolveCoursePath(dir, relativePath)` | Resolve a path safely inside the course directory |
|
|
86
102
|
| `isPathContained(root, target)` | Whether `target` stays under `root` |
|
|
87
103
|
| `courseManifestSchema` | Zod schema for the full course manifest |
|
|
88
|
-
| `lessonSchema`, `assessmentSchema`, … | Strict sub-schemas
|
|
89
|
-
| `CourseManifest`, `Lesson`, `Assessment`, `
|
|
104
|
+
| `lessonSchema`, `assessmentSchema`, `variableDefSchema`, … | Strict sub-schemas |
|
|
105
|
+
| `CourseManifest`, `Lesson`, `Assessment`, `FlowRule`, `VariableDef`, `RuntimeAssessmentBundle` | TypeScript types |
|
|
90
106
|
|
|
91
107
|
## What gets validated
|
|
92
108
|
|
|
93
|
-
- Manifest shape
|
|
94
|
-
- Lesson types: `markdown` (`file`)
|
|
95
|
-
-
|
|
109
|
+
- Manifest shape: lessons, assessments, optional `variables` and `flow`, tracking rules
|
|
110
|
+
- Lesson types: `markdown` (`file`), `html` (`path`), `component` (`component` + optional `props`)
|
|
111
|
+
- Component IDs: built-in IDs or course overrides under `components/<id>/`
|
|
112
|
+
- Flow rules: condition grammar, `goto` targets that reference known activity IDs, acyclic flow (warnings for cycles)
|
|
113
|
+
- Assessment YAML: strict MCQ schemas; optional `maxAttempts`, `shuffleChoices`, `showFeedback`; `explanation` per question
|
|
96
114
|
- Duplicate lesson IDs
|
|
97
115
|
- Path containment — referenced files must stay inside the course directory (including via symlinks)
|
|
98
|
-
- On-disk assets: files exist and
|
|
116
|
+
- On-disk assets: files exist and assessment paths are regular files
|
|
99
117
|
|
|
100
118
|
## Assessment packaging
|
|
101
119
|
|
|
102
120
|
Author assessments live as YAML under `assessments/` in the course repo. At build/preview time:
|
|
103
121
|
|
|
104
122
|
1. `buildRuntimeAssessmentBundle()` reads each assessment file.
|
|
105
|
-
2. **Learner payload** — questions and choices without `correct`
|
|
123
|
+
2. **Learner payload** — questions and choices without `correct` flags.
|
|
106
124
|
3. **Answer keys** — `questionId → choiceId` map for scoring.
|
|
125
|
+
4. **Configs** — per-assessment quiz behavior (`maxAttempts`, `shuffleChoices`, `showFeedback`).
|
|
126
|
+
5. **Feedback** — `questionId → explanation` for immediate/end feedback (not shipped as separate files).
|
|
107
127
|
|
|
108
|
-
The CLI and [`@lxpack/scorm`](../scorm/README.md) embed
|
|
128
|
+
The CLI and [`@lxpack/scorm`](../scorm/README.md) embed all of this in the HTML config JSON. Exported ZIPs **do not** include `assessments/` files, so answer keys are not fetchable as static assets.
|
|
109
129
|
|
|
110
130
|
## Development
|
|
111
131
|
|
|
@@ -120,6 +140,8 @@ pnpm --filter @lxpack/validators typecheck
|
|
|
120
140
|
## Links
|
|
121
141
|
|
|
122
142
|
- [LXPack repository](https://github.com/eddiethedean/lxpack)
|
|
143
|
+
- [Documentation index](https://github.com/eddiethedean/lxpack/blob/main/docs/README.md)
|
|
144
|
+
- [Technical specification](https://github.com/eddiethedean/lxpack/blob/main/docs/SPEC.md)
|
|
123
145
|
- [Changelog](https://github.com/eddiethedean/lxpack/blob/main/CHANGELOG.md)
|
|
124
146
|
|
|
125
147
|
## License
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
type Condition = {
|
|
4
|
+
variable: {
|
|
5
|
+
eq: [string, string | number | boolean];
|
|
6
|
+
};
|
|
7
|
+
} | {
|
|
8
|
+
assessment: {
|
|
9
|
+
passed: string;
|
|
10
|
+
};
|
|
11
|
+
} | {
|
|
12
|
+
interaction: {
|
|
13
|
+
done: string;
|
|
14
|
+
};
|
|
15
|
+
} | {
|
|
16
|
+
all: Condition[];
|
|
17
|
+
} | {
|
|
18
|
+
any: Condition[];
|
|
19
|
+
};
|
|
20
|
+
declare const conditionSchema: z.ZodType<Condition>;
|
|
21
|
+
declare const flowRuleSchema: z.ZodObject<{
|
|
22
|
+
when: z.ZodType<Condition, z.ZodTypeDef, Condition>;
|
|
23
|
+
goto: z.ZodString;
|
|
24
|
+
}, "strict", z.ZodTypeAny, {
|
|
25
|
+
when: Condition;
|
|
26
|
+
goto: string;
|
|
27
|
+
}, {
|
|
28
|
+
when: Condition;
|
|
29
|
+
goto: string;
|
|
30
|
+
}>;
|
|
31
|
+
type FlowRule = z.infer<typeof flowRuleSchema>;
|
|
32
|
+
|
|
3
33
|
declare const assessmentQuestionSchema: z.ZodEffects<z.ZodObject<{
|
|
4
34
|
id: z.ZodString;
|
|
5
35
|
prompt: z.ZodString;
|
|
@@ -60,13 +90,13 @@ declare const markdownLessonSchema: z.ZodObject<{
|
|
|
60
90
|
file: z.ZodString;
|
|
61
91
|
title: z.ZodOptional<z.ZodString>;
|
|
62
92
|
}, "strict", z.ZodTypeAny, {
|
|
63
|
-
id: string;
|
|
64
93
|
type: "markdown";
|
|
94
|
+
id: string;
|
|
65
95
|
file: string;
|
|
66
96
|
title?: string | undefined;
|
|
67
97
|
}, {
|
|
68
|
-
id: string;
|
|
69
98
|
type: "markdown";
|
|
99
|
+
id: string;
|
|
70
100
|
file: string;
|
|
71
101
|
title?: string | undefined;
|
|
72
102
|
}>;
|
|
@@ -76,29 +106,48 @@ declare const htmlLessonSchema: z.ZodObject<{
|
|
|
76
106
|
path: z.ZodString;
|
|
77
107
|
title: z.ZodOptional<z.ZodString>;
|
|
78
108
|
}, "strict", z.ZodTypeAny, {
|
|
79
|
-
id: string;
|
|
80
109
|
path: string;
|
|
81
110
|
type: "html";
|
|
111
|
+
id: string;
|
|
82
112
|
title?: string | undefined;
|
|
83
113
|
}, {
|
|
84
|
-
id: string;
|
|
85
114
|
path: string;
|
|
86
115
|
type: "html";
|
|
116
|
+
id: string;
|
|
87
117
|
title?: string | undefined;
|
|
88
118
|
}>;
|
|
119
|
+
declare const componentLessonSchema: z.ZodObject<{
|
|
120
|
+
id: z.ZodString;
|
|
121
|
+
type: z.ZodLiteral<"component">;
|
|
122
|
+
component: z.ZodString;
|
|
123
|
+
props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
124
|
+
title: z.ZodOptional<z.ZodString>;
|
|
125
|
+
}, "strict", z.ZodTypeAny, {
|
|
126
|
+
type: "component";
|
|
127
|
+
id: string;
|
|
128
|
+
component: string;
|
|
129
|
+
title?: string | undefined;
|
|
130
|
+
props?: Record<string, unknown> | undefined;
|
|
131
|
+
}, {
|
|
132
|
+
type: "component";
|
|
133
|
+
id: string;
|
|
134
|
+
component: string;
|
|
135
|
+
title?: string | undefined;
|
|
136
|
+
props?: Record<string, unknown> | undefined;
|
|
137
|
+
}>;
|
|
89
138
|
declare const lessonSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
90
139
|
id: z.ZodString;
|
|
91
140
|
type: z.ZodLiteral<"markdown">;
|
|
92
141
|
file: z.ZodString;
|
|
93
142
|
title: z.ZodOptional<z.ZodString>;
|
|
94
143
|
}, "strict", z.ZodTypeAny, {
|
|
95
|
-
id: string;
|
|
96
144
|
type: "markdown";
|
|
145
|
+
id: string;
|
|
97
146
|
file: string;
|
|
98
147
|
title?: string | undefined;
|
|
99
148
|
}, {
|
|
100
|
-
id: string;
|
|
101
149
|
type: "markdown";
|
|
150
|
+
id: string;
|
|
102
151
|
file: string;
|
|
103
152
|
title?: string | undefined;
|
|
104
153
|
}>, z.ZodObject<{
|
|
@@ -107,20 +156,42 @@ declare const lessonSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
|
107
156
|
path: z.ZodString;
|
|
108
157
|
title: z.ZodOptional<z.ZodString>;
|
|
109
158
|
}, "strict", z.ZodTypeAny, {
|
|
110
|
-
id: string;
|
|
111
159
|
path: string;
|
|
112
160
|
type: "html";
|
|
161
|
+
id: string;
|
|
113
162
|
title?: string | undefined;
|
|
114
163
|
}, {
|
|
115
|
-
id: string;
|
|
116
164
|
path: string;
|
|
117
165
|
type: "html";
|
|
166
|
+
id: string;
|
|
118
167
|
title?: string | undefined;
|
|
168
|
+
}>, z.ZodObject<{
|
|
169
|
+
id: z.ZodString;
|
|
170
|
+
type: z.ZodLiteral<"component">;
|
|
171
|
+
component: z.ZodString;
|
|
172
|
+
props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
173
|
+
title: z.ZodOptional<z.ZodString>;
|
|
174
|
+
}, "strict", z.ZodTypeAny, {
|
|
175
|
+
type: "component";
|
|
176
|
+
id: string;
|
|
177
|
+
component: string;
|
|
178
|
+
title?: string | undefined;
|
|
179
|
+
props?: Record<string, unknown> | undefined;
|
|
180
|
+
}, {
|
|
181
|
+
type: "component";
|
|
182
|
+
id: string;
|
|
183
|
+
component: string;
|
|
184
|
+
title?: string | undefined;
|
|
185
|
+
props?: Record<string, unknown> | undefined;
|
|
119
186
|
}>]>;
|
|
187
|
+
declare const showFeedbackSchema: z.ZodDefault<z.ZodEnum<["immediate", "end", "never"]>>;
|
|
120
188
|
declare const assessmentSchema: z.ZodObject<{
|
|
121
189
|
id: z.ZodString;
|
|
122
190
|
title: z.ZodOptional<z.ZodString>;
|
|
123
191
|
passingScore: z.ZodDefault<z.ZodNumber>;
|
|
192
|
+
maxAttempts: z.ZodOptional<z.ZodNumber>;
|
|
193
|
+
shuffleChoices: z.ZodOptional<z.ZodBoolean>;
|
|
194
|
+
showFeedback: z.ZodOptional<z.ZodDefault<z.ZodEnum<["immediate", "end", "never"]>>>;
|
|
124
195
|
questions: z.ZodArray<z.ZodEffects<z.ZodObject<{
|
|
125
196
|
id: z.ZodString;
|
|
126
197
|
prompt: z.ZodString;
|
|
@@ -189,6 +260,9 @@ declare const assessmentSchema: z.ZodObject<{
|
|
|
189
260
|
explanation?: string | undefined;
|
|
190
261
|
}[];
|
|
191
262
|
title?: string | undefined;
|
|
263
|
+
maxAttempts?: number | undefined;
|
|
264
|
+
shuffleChoices?: boolean | undefined;
|
|
265
|
+
showFeedback?: "never" | "immediate" | "end" | undefined;
|
|
192
266
|
}, {
|
|
193
267
|
id: string;
|
|
194
268
|
questions: {
|
|
@@ -203,6 +277,9 @@ declare const assessmentSchema: z.ZodObject<{
|
|
|
203
277
|
}[];
|
|
204
278
|
title?: string | undefined;
|
|
205
279
|
passingScore?: number | undefined;
|
|
280
|
+
maxAttempts?: number | undefined;
|
|
281
|
+
shuffleChoices?: boolean | undefined;
|
|
282
|
+
showFeedback?: "never" | "immediate" | "end" | undefined;
|
|
206
283
|
}>;
|
|
207
284
|
declare const assessmentRefSchema: z.ZodObject<{
|
|
208
285
|
id: z.ZodString;
|
|
@@ -214,6 +291,16 @@ declare const assessmentRefSchema: z.ZodObject<{
|
|
|
214
291
|
id: string;
|
|
215
292
|
file: string;
|
|
216
293
|
}>;
|
|
294
|
+
declare const variableDefSchema: z.ZodObject<{
|
|
295
|
+
default: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
296
|
+
type: z.ZodOptional<z.ZodEnum<["string", "number", "boolean"]>>;
|
|
297
|
+
}, "strict", z.ZodTypeAny, {
|
|
298
|
+
default: string | number | boolean;
|
|
299
|
+
type?: "string" | "number" | "boolean" | undefined;
|
|
300
|
+
}, {
|
|
301
|
+
default: string | number | boolean;
|
|
302
|
+
type?: "string" | "number" | "boolean" | undefined;
|
|
303
|
+
}>;
|
|
217
304
|
declare const trackingSchema: z.ZodOptional<z.ZodObject<{
|
|
218
305
|
completion: z.ZodOptional<z.ZodObject<{
|
|
219
306
|
threshold: z.ZodDefault<z.ZodNumber>;
|
|
@@ -238,6 +325,7 @@ declare const runtimeConfigSchema: z.ZodOptional<z.ZodObject<{
|
|
|
238
325
|
}, {
|
|
239
326
|
theme?: string | undefined;
|
|
240
327
|
}>>;
|
|
328
|
+
|
|
241
329
|
declare const courseManifestSchema: z.ZodObject<{
|
|
242
330
|
title: z.ZodString;
|
|
243
331
|
version: z.ZodString;
|
|
@@ -266,19 +354,39 @@ declare const courseManifestSchema: z.ZodObject<{
|
|
|
266
354
|
threshold?: number | undefined;
|
|
267
355
|
} | undefined;
|
|
268
356
|
}>>;
|
|
357
|
+
variables: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
358
|
+
default: z.ZodUnion<[z.ZodString, z.ZodNumber, z.ZodBoolean]>;
|
|
359
|
+
type: z.ZodOptional<z.ZodEnum<["string", "number", "boolean"]>>;
|
|
360
|
+
}, "strict", z.ZodTypeAny, {
|
|
361
|
+
default: string | number | boolean;
|
|
362
|
+
type?: "string" | "number" | "boolean" | undefined;
|
|
363
|
+
}, {
|
|
364
|
+
default: string | number | boolean;
|
|
365
|
+
type?: "string" | "number" | "boolean" | undefined;
|
|
366
|
+
}>>>;
|
|
367
|
+
flow: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
368
|
+
when: z.ZodType<Condition, z.ZodTypeDef, Condition>;
|
|
369
|
+
goto: z.ZodString;
|
|
370
|
+
}, "strict", z.ZodTypeAny, {
|
|
371
|
+
when: Condition;
|
|
372
|
+
goto: string;
|
|
373
|
+
}, {
|
|
374
|
+
when: Condition;
|
|
375
|
+
goto: string;
|
|
376
|
+
}>, "many">>;
|
|
269
377
|
lessons: z.ZodArray<z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
270
378
|
id: z.ZodString;
|
|
271
379
|
type: z.ZodLiteral<"markdown">;
|
|
272
380
|
file: z.ZodString;
|
|
273
381
|
title: z.ZodOptional<z.ZodString>;
|
|
274
382
|
}, "strict", z.ZodTypeAny, {
|
|
275
|
-
id: string;
|
|
276
383
|
type: "markdown";
|
|
384
|
+
id: string;
|
|
277
385
|
file: string;
|
|
278
386
|
title?: string | undefined;
|
|
279
387
|
}, {
|
|
280
|
-
id: string;
|
|
281
388
|
type: "markdown";
|
|
389
|
+
id: string;
|
|
282
390
|
file: string;
|
|
283
391
|
title?: string | undefined;
|
|
284
392
|
}>, z.ZodObject<{
|
|
@@ -287,15 +395,33 @@ declare const courseManifestSchema: z.ZodObject<{
|
|
|
287
395
|
path: z.ZodString;
|
|
288
396
|
title: z.ZodOptional<z.ZodString>;
|
|
289
397
|
}, "strict", z.ZodTypeAny, {
|
|
290
|
-
id: string;
|
|
291
398
|
path: string;
|
|
292
399
|
type: "html";
|
|
400
|
+
id: string;
|
|
293
401
|
title?: string | undefined;
|
|
294
402
|
}, {
|
|
295
|
-
id: string;
|
|
296
403
|
path: string;
|
|
297
404
|
type: "html";
|
|
405
|
+
id: string;
|
|
406
|
+
title?: string | undefined;
|
|
407
|
+
}>, z.ZodObject<{
|
|
408
|
+
id: z.ZodString;
|
|
409
|
+
type: z.ZodLiteral<"component">;
|
|
410
|
+
component: z.ZodString;
|
|
411
|
+
props: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
412
|
+
title: z.ZodOptional<z.ZodString>;
|
|
413
|
+
}, "strict", z.ZodTypeAny, {
|
|
414
|
+
type: "component";
|
|
415
|
+
id: string;
|
|
416
|
+
component: string;
|
|
417
|
+
title?: string | undefined;
|
|
418
|
+
props?: Record<string, unknown> | undefined;
|
|
419
|
+
}, {
|
|
420
|
+
type: "component";
|
|
421
|
+
id: string;
|
|
422
|
+
component: string;
|
|
298
423
|
title?: string | undefined;
|
|
424
|
+
props?: Record<string, unknown> | undefined;
|
|
299
425
|
}>]>, "many">;
|
|
300
426
|
assessments: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
301
427
|
id: z.ZodString;
|
|
@@ -311,15 +437,21 @@ declare const courseManifestSchema: z.ZodObject<{
|
|
|
311
437
|
title: string;
|
|
312
438
|
version: string;
|
|
313
439
|
lessons: ({
|
|
314
|
-
id: string;
|
|
315
440
|
type: "markdown";
|
|
441
|
+
id: string;
|
|
316
442
|
file: string;
|
|
317
443
|
title?: string | undefined;
|
|
318
444
|
} | {
|
|
319
|
-
id: string;
|
|
320
445
|
path: string;
|
|
321
446
|
type: "html";
|
|
447
|
+
id: string;
|
|
322
448
|
title?: string | undefined;
|
|
449
|
+
} | {
|
|
450
|
+
type: "component";
|
|
451
|
+
id: string;
|
|
452
|
+
component: string;
|
|
453
|
+
title?: string | undefined;
|
|
454
|
+
props?: Record<string, unknown> | undefined;
|
|
323
455
|
})[];
|
|
324
456
|
description?: string | undefined;
|
|
325
457
|
runtime?: {
|
|
@@ -330,6 +462,14 @@ declare const courseManifestSchema: z.ZodObject<{
|
|
|
330
462
|
threshold: number;
|
|
331
463
|
} | undefined;
|
|
332
464
|
} | undefined;
|
|
465
|
+
variables?: Record<string, {
|
|
466
|
+
default: string | number | boolean;
|
|
467
|
+
type?: "string" | "number" | "boolean" | undefined;
|
|
468
|
+
}> | undefined;
|
|
469
|
+
flow?: {
|
|
470
|
+
when: Condition;
|
|
471
|
+
goto: string;
|
|
472
|
+
}[] | undefined;
|
|
333
473
|
assessments?: {
|
|
334
474
|
id: string;
|
|
335
475
|
file: string;
|
|
@@ -338,15 +478,21 @@ declare const courseManifestSchema: z.ZodObject<{
|
|
|
338
478
|
title: string;
|
|
339
479
|
version: string;
|
|
340
480
|
lessons: ({
|
|
341
|
-
id: string;
|
|
342
481
|
type: "markdown";
|
|
482
|
+
id: string;
|
|
343
483
|
file: string;
|
|
344
484
|
title?: string | undefined;
|
|
345
485
|
} | {
|
|
346
|
-
id: string;
|
|
347
486
|
path: string;
|
|
348
487
|
type: "html";
|
|
488
|
+
id: string;
|
|
489
|
+
title?: string | undefined;
|
|
490
|
+
} | {
|
|
491
|
+
type: "component";
|
|
492
|
+
id: string;
|
|
493
|
+
component: string;
|
|
349
494
|
title?: string | undefined;
|
|
495
|
+
props?: Record<string, unknown> | undefined;
|
|
350
496
|
})[];
|
|
351
497
|
description?: string | undefined;
|
|
352
498
|
runtime?: {
|
|
@@ -357,6 +503,14 @@ declare const courseManifestSchema: z.ZodObject<{
|
|
|
357
503
|
threshold?: number | undefined;
|
|
358
504
|
} | undefined;
|
|
359
505
|
} | undefined;
|
|
506
|
+
variables?: Record<string, {
|
|
507
|
+
default: string | number | boolean;
|
|
508
|
+
type?: "string" | "number" | "boolean" | undefined;
|
|
509
|
+
}> | undefined;
|
|
510
|
+
flow?: {
|
|
511
|
+
when: Condition;
|
|
512
|
+
goto: string;
|
|
513
|
+
}[] | undefined;
|
|
360
514
|
assessments?: {
|
|
361
515
|
id: string;
|
|
362
516
|
file: string;
|
|
@@ -366,6 +520,14 @@ type CourseManifest = z.infer<typeof courseManifestSchema>;
|
|
|
366
520
|
type Lesson = z.infer<typeof lessonSchema>;
|
|
367
521
|
type Assessment = z.infer<typeof assessmentSchema>;
|
|
368
522
|
type AssessmentRef = z.infer<typeof assessmentRefSchema>;
|
|
523
|
+
type VariableDef = z.infer<typeof variableDefSchema>;
|
|
524
|
+
type ComponentLesson = z.infer<typeof componentLessonSchema>;
|
|
525
|
+
type ShowFeedback = z.infer<typeof showFeedbackSchema>;
|
|
526
|
+
|
|
527
|
+
/** Built-in component ids shipped in @lxpack/components */
|
|
528
|
+
declare const BUILTIN_COMPONENT_IDS: readonly ["callout", "image-card", "checklist"];
|
|
529
|
+
type BuiltinComponentId = (typeof BUILTIN_COMPONENT_IDS)[number];
|
|
530
|
+
declare function isBuiltinComponentId(id: string): id is BuiltinComponentId;
|
|
369
531
|
|
|
370
532
|
declare function formatErrorMessage(err: unknown): string;
|
|
371
533
|
declare function formatIssuePath(path: PropertyKey[]): string;
|
|
@@ -399,6 +561,11 @@ declare function loadManifest(courseDir: string): Promise<{
|
|
|
399
561
|
} | ValidationIssue[]>;
|
|
400
562
|
declare function validateCourse(courseDir: string): Promise<ValidationResult>;
|
|
401
563
|
|
|
564
|
+
declare function collectActivityIds(manifest: CourseManifest): Set<string>;
|
|
565
|
+
declare function validateFlow(manifest: CourseManifest): ValidationIssue[];
|
|
566
|
+
/** Detect simple goto cycles (same-target chains). */
|
|
567
|
+
declare function detectFlowCycles(flow: FlowRule[]): string[];
|
|
568
|
+
|
|
402
569
|
interface LearnerChoice {
|
|
403
570
|
id: string;
|
|
404
571
|
text: string;
|
|
@@ -414,14 +581,26 @@ interface LearnerAssessment {
|
|
|
414
581
|
passingScore: number;
|
|
415
582
|
questions: LearnerQuestion[];
|
|
416
583
|
}
|
|
584
|
+
interface AssessmentRuntimeConfig {
|
|
585
|
+
maxAttempts: number;
|
|
586
|
+
shuffleChoices: boolean;
|
|
587
|
+
showFeedback: ShowFeedback;
|
|
588
|
+
}
|
|
589
|
+
interface QuestionFeedback {
|
|
590
|
+
[questionId: string]: string | undefined;
|
|
591
|
+
}
|
|
417
592
|
interface RuntimeAssessmentBundle {
|
|
418
593
|
assessments: Record<string, LearnerAssessment>;
|
|
419
594
|
answerKeys: Record<string, Record<string, string>>;
|
|
595
|
+
configs: Record<string, AssessmentRuntimeConfig>;
|
|
596
|
+
feedback: Record<string, QuestionFeedback>;
|
|
420
597
|
}
|
|
421
598
|
declare function toLearnerAssessment(assessment: Assessment): {
|
|
422
599
|
learner: LearnerAssessment;
|
|
423
600
|
answerKey: Record<string, string>;
|
|
601
|
+
config: AssessmentRuntimeConfig;
|
|
602
|
+
feedback: QuestionFeedback;
|
|
424
603
|
};
|
|
425
604
|
declare function buildRuntimeAssessmentBundle(courseDir: string, manifest: CourseManifest): Promise<RuntimeAssessmentBundle>;
|
|
426
605
|
|
|
427
|
-
export { type Assessment, type AssessmentRef, type CourseManifest, type LearnerAssessment, type LearnerChoice, type LearnerQuestion, type Lesson, type RuntimeAssessmentBundle, type ValidationIssue, type ValidationResult, assertResolvedPathContained, assessmentQuestionSchema, assessmentRefSchema, assessmentSchema, buildRuntimeAssessmentBundle, courseManifestSchema, formatErrorMessage, formatIssuePath, htmlLessonSchema, isPathContained, lessonSchema, loadManifest, markdownLessonSchema, resolveCoursePath, runtimeConfigSchema, toLearnerAssessment, trackingSchema, validateCourse };
|
|
606
|
+
export { type Assessment, type AssessmentRef, type AssessmentRuntimeConfig, BUILTIN_COMPONENT_IDS, type BuiltinComponentId, type ComponentLesson, type Condition, type CourseManifest, type FlowRule, type LearnerAssessment, type LearnerChoice, type LearnerQuestion, type Lesson, type QuestionFeedback, type RuntimeAssessmentBundle, type ShowFeedback, type ValidationIssue, type ValidationResult, type VariableDef, assertResolvedPathContained, assessmentQuestionSchema, assessmentRefSchema, assessmentSchema, buildRuntimeAssessmentBundle, collectActivityIds, componentLessonSchema, conditionSchema, courseManifestSchema, detectFlowCycles, flowRuleSchema, formatErrorMessage, formatIssuePath, htmlLessonSchema, isBuiltinComponentId, isPathContained, lessonSchema, loadManifest, markdownLessonSchema, resolveCoursePath, runtimeConfigSchema, showFeedbackSchema, toLearnerAssessment, trackingSchema, validateCourse, validateFlow, variableDefSchema };
|
package/dist/index.js
CHANGED
|
@@ -1,69 +1,240 @@
|
|
|
1
1
|
// src/schemas.ts
|
|
2
|
+
import { z as z2 } from "zod";
|
|
3
|
+
|
|
4
|
+
// src/conditions.ts
|
|
2
5
|
import { z } from "zod";
|
|
3
|
-
var
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
var baseConditionSchema = z.union([
|
|
7
|
+
z.object({
|
|
8
|
+
variable: z.object({
|
|
9
|
+
eq: z.tuple([
|
|
10
|
+
z.string().min(1),
|
|
11
|
+
z.union([z.string(), z.number(), z.boolean()])
|
|
12
|
+
])
|
|
13
|
+
})
|
|
14
|
+
}).strict(),
|
|
15
|
+
z.object({
|
|
16
|
+
assessment: z.object({
|
|
17
|
+
passed: z.string().min(1)
|
|
18
|
+
})
|
|
19
|
+
}).strict(),
|
|
20
|
+
z.object({
|
|
21
|
+
interaction: z.object({
|
|
22
|
+
done: z.string().min(1)
|
|
23
|
+
})
|
|
24
|
+
}).strict()
|
|
25
|
+
]);
|
|
26
|
+
var conditionSchema = z.lazy(
|
|
27
|
+
() => z.union([
|
|
28
|
+
...baseConditionSchema.options,
|
|
29
|
+
z.object({ all: z.array(conditionSchema) }).strict(),
|
|
30
|
+
z.object({ any: z.array(conditionSchema) }).strict()
|
|
31
|
+
])
|
|
32
|
+
);
|
|
33
|
+
var flowRuleSchema = z.object({
|
|
34
|
+
when: conditionSchema,
|
|
35
|
+
goto: z.string().min(1)
|
|
36
|
+
}).strict();
|
|
37
|
+
|
|
38
|
+
// src/schemas.ts
|
|
39
|
+
var choiceSchema = z2.object({
|
|
40
|
+
id: z2.string().min(1),
|
|
41
|
+
text: z2.string().min(1),
|
|
42
|
+
correct: z2.boolean().optional()
|
|
7
43
|
}).strict();
|
|
8
|
-
var assessmentQuestionSchema =
|
|
9
|
-
id:
|
|
10
|
-
prompt:
|
|
11
|
-
choices:
|
|
12
|
-
explanation:
|
|
44
|
+
var assessmentQuestionSchema = z2.object({
|
|
45
|
+
id: z2.string().min(1),
|
|
46
|
+
prompt: z2.string().min(1),
|
|
47
|
+
choices: z2.array(choiceSchema).min(1),
|
|
48
|
+
explanation: z2.string().optional()
|
|
13
49
|
}).strict().superRefine((question, ctx) => {
|
|
14
50
|
const correctCount = question.choices.filter((c) => c.correct === true).length;
|
|
15
51
|
if (correctCount !== 1) {
|
|
16
52
|
ctx.addIssue({
|
|
17
|
-
code:
|
|
53
|
+
code: z2.ZodIssueCode.custom,
|
|
18
54
|
message: "Each question must have exactly one correct choice",
|
|
19
55
|
path: ["choices"]
|
|
20
56
|
});
|
|
21
57
|
}
|
|
22
58
|
});
|
|
23
|
-
var markdownLessonSchema =
|
|
24
|
-
id:
|
|
25
|
-
type:
|
|
26
|
-
file:
|
|
27
|
-
title:
|
|
59
|
+
var markdownLessonSchema = z2.object({
|
|
60
|
+
id: z2.string().min(1),
|
|
61
|
+
type: z2.literal("markdown"),
|
|
62
|
+
file: z2.string().min(1),
|
|
63
|
+
title: z2.string().optional()
|
|
64
|
+
}).strict();
|
|
65
|
+
var htmlLessonSchema = z2.object({
|
|
66
|
+
id: z2.string().min(1),
|
|
67
|
+
type: z2.literal("html"),
|
|
68
|
+
path: z2.string().min(1),
|
|
69
|
+
title: z2.string().optional()
|
|
28
70
|
}).strict();
|
|
29
|
-
var
|
|
30
|
-
id:
|
|
31
|
-
type:
|
|
32
|
-
|
|
33
|
-
|
|
71
|
+
var componentLessonSchema = z2.object({
|
|
72
|
+
id: z2.string().min(1),
|
|
73
|
+
type: z2.literal("component"),
|
|
74
|
+
component: z2.string().min(1),
|
|
75
|
+
props: z2.record(z2.unknown()).optional(),
|
|
76
|
+
title: z2.string().optional()
|
|
34
77
|
}).strict();
|
|
35
|
-
var lessonSchema =
|
|
78
|
+
var lessonSchema = z2.discriminatedUnion("type", [
|
|
36
79
|
markdownLessonSchema,
|
|
37
|
-
htmlLessonSchema
|
|
80
|
+
htmlLessonSchema,
|
|
81
|
+
componentLessonSchema
|
|
38
82
|
]);
|
|
39
|
-
var
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
83
|
+
var showFeedbackSchema = z2.enum(["immediate", "end", "never"]).default("never");
|
|
84
|
+
var assessmentSchema = z2.object({
|
|
85
|
+
id: z2.string().min(1),
|
|
86
|
+
title: z2.string().optional(),
|
|
87
|
+
passingScore: z2.number().min(0).max(1).default(0.7),
|
|
88
|
+
maxAttempts: z2.number().int().min(1).optional(),
|
|
89
|
+
shuffleChoices: z2.boolean().optional(),
|
|
90
|
+
showFeedback: showFeedbackSchema.optional(),
|
|
91
|
+
questions: z2.array(assessmentQuestionSchema).min(1)
|
|
92
|
+
}).strict();
|
|
93
|
+
var assessmentRefSchema = z2.object({
|
|
94
|
+
id: z2.string().min(1),
|
|
95
|
+
file: z2.string().min(1)
|
|
44
96
|
}).strict();
|
|
45
|
-
var
|
|
46
|
-
|
|
47
|
-
|
|
97
|
+
var variableDefSchema = z2.object({
|
|
98
|
+
default: z2.union([z2.string(), z2.number(), z2.boolean()]),
|
|
99
|
+
type: z2.enum(["string", "number", "boolean"]).optional()
|
|
48
100
|
}).strict();
|
|
49
|
-
var trackingSchema =
|
|
50
|
-
completion:
|
|
51
|
-
threshold:
|
|
101
|
+
var trackingSchema = z2.object({
|
|
102
|
+
completion: z2.object({
|
|
103
|
+
threshold: z2.number().min(0).max(1).default(0.9)
|
|
52
104
|
}).strict().optional()
|
|
53
105
|
}).strict().optional();
|
|
54
|
-
var runtimeConfigSchema =
|
|
55
|
-
theme:
|
|
106
|
+
var runtimeConfigSchema = z2.object({
|
|
107
|
+
theme: z2.string().default("modern")
|
|
56
108
|
}).strict().optional();
|
|
57
|
-
var courseManifestSchema =
|
|
58
|
-
title:
|
|
59
|
-
version:
|
|
60
|
-
description:
|
|
109
|
+
var courseManifestSchema = z2.object({
|
|
110
|
+
title: z2.string().min(1),
|
|
111
|
+
version: z2.string().min(1),
|
|
112
|
+
description: z2.string().optional(),
|
|
61
113
|
runtime: runtimeConfigSchema,
|
|
62
114
|
tracking: trackingSchema,
|
|
63
|
-
|
|
64
|
-
|
|
115
|
+
variables: z2.record(variableDefSchema).optional(),
|
|
116
|
+
flow: z2.array(flowRuleSchema).optional(),
|
|
117
|
+
lessons: z2.array(lessonSchema).min(1),
|
|
118
|
+
assessments: z2.array(assessmentRefSchema).optional()
|
|
65
119
|
}).strict();
|
|
66
120
|
|
|
121
|
+
// src/components.ts
|
|
122
|
+
var BUILTIN_COMPONENT_IDS = [
|
|
123
|
+
"callout",
|
|
124
|
+
"image-card",
|
|
125
|
+
"checklist"
|
|
126
|
+
];
|
|
127
|
+
function isBuiltinComponentId(id) {
|
|
128
|
+
return BUILTIN_COMPONENT_IDS.includes(id);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// src/flow-validate.ts
|
|
132
|
+
function collectActivityIds(manifest) {
|
|
133
|
+
const ids = /* @__PURE__ */ new Set();
|
|
134
|
+
for (const lesson of manifest.lessons) {
|
|
135
|
+
ids.add(lesson.id);
|
|
136
|
+
}
|
|
137
|
+
for (const ref of manifest.assessments ?? []) {
|
|
138
|
+
ids.add(ref.id);
|
|
139
|
+
}
|
|
140
|
+
return ids;
|
|
141
|
+
}
|
|
142
|
+
function collectConditionRefs(condition, refs) {
|
|
143
|
+
if ("variable" in condition && condition.variable?.eq) {
|
|
144
|
+
refs.variables.add(condition.variable.eq[0]);
|
|
145
|
+
}
|
|
146
|
+
if ("assessment" in condition && condition.assessment?.passed) {
|
|
147
|
+
refs.assessments.add(condition.assessment.passed);
|
|
148
|
+
}
|
|
149
|
+
if ("interaction" in condition && condition.interaction?.done) {
|
|
150
|
+
refs.interactions.add(condition.interaction.done);
|
|
151
|
+
}
|
|
152
|
+
if ("all" in condition && condition.all) {
|
|
153
|
+
for (const c of condition.all) collectConditionRefs(c, refs);
|
|
154
|
+
}
|
|
155
|
+
if ("any" in condition && condition.any) {
|
|
156
|
+
for (const c of condition.any) collectConditionRefs(c, refs);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function validateFlow(manifest) {
|
|
160
|
+
const issues = [];
|
|
161
|
+
const flow = manifest.flow;
|
|
162
|
+
if (!flow?.length) return issues;
|
|
163
|
+
const activityIds = collectActivityIds(manifest);
|
|
164
|
+
const manifestVars = new Set(Object.keys(manifest.variables ?? {}));
|
|
165
|
+
flow.forEach((rule, index) => {
|
|
166
|
+
const path = `flow[${index}]`;
|
|
167
|
+
if (!activityIds.has(rule.goto)) {
|
|
168
|
+
issues.push({
|
|
169
|
+
path: `${path}.goto`,
|
|
170
|
+
message: `Unknown activity id: ${rule.goto}`,
|
|
171
|
+
severity: "error"
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
const refs = {
|
|
175
|
+
variables: /* @__PURE__ */ new Set(),
|
|
176
|
+
assessments: /* @__PURE__ */ new Set(),
|
|
177
|
+
interactions: /* @__PURE__ */ new Set()
|
|
178
|
+
};
|
|
179
|
+
collectConditionRefs(rule.when, refs);
|
|
180
|
+
for (const v of refs.variables) {
|
|
181
|
+
if (!manifestVars.has(v)) {
|
|
182
|
+
issues.push({
|
|
183
|
+
path: `${path}.when`,
|
|
184
|
+
message: `Unknown variable in condition: ${v}`,
|
|
185
|
+
severity: "error"
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const a of refs.assessments) {
|
|
190
|
+
if (!activityIds.has(a)) {
|
|
191
|
+
issues.push({
|
|
192
|
+
path: `${path}.when`,
|
|
193
|
+
message: `Unknown assessment in condition: ${a}`,
|
|
194
|
+
severity: "error"
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const i of refs.interactions) {
|
|
199
|
+
if (!activityIds.has(i)) {
|
|
200
|
+
issues.push({
|
|
201
|
+
path: `${path}.when`,
|
|
202
|
+
message: `Unknown interaction/lesson id in condition: ${i}`,
|
|
203
|
+
severity: "warning"
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
return issues;
|
|
209
|
+
}
|
|
210
|
+
function detectFlowCycles(flow) {
|
|
211
|
+
const gotoOf = /* @__PURE__ */ new Map();
|
|
212
|
+
flow.forEach((rule, i) => gotoOf.set(i, rule.goto));
|
|
213
|
+
const errors = [];
|
|
214
|
+
const visited = /* @__PURE__ */ new Set();
|
|
215
|
+
for (let start = 0; start < flow.length; start++) {
|
|
216
|
+
if (visited.has(start)) continue;
|
|
217
|
+
const chain = /* @__PURE__ */ new Set();
|
|
218
|
+
let i = start;
|
|
219
|
+
while (i !== void 0 && i < flow.length) {
|
|
220
|
+
const ruleIndex = i;
|
|
221
|
+
if (chain.has(ruleIndex)) {
|
|
222
|
+
errors.push(`Flow rule cycle detected involving flow[${ruleIndex}]`);
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
if (visited.has(ruleIndex)) break;
|
|
226
|
+
chain.add(ruleIndex);
|
|
227
|
+
visited.add(ruleIndex);
|
|
228
|
+
const target = gotoOf.get(ruleIndex);
|
|
229
|
+
const nextIdx = flow.findIndex(
|
|
230
|
+
(_, idx) => idx > ruleIndex && flow[idx].goto === target
|
|
231
|
+
);
|
|
232
|
+
i = nextIdx >= 0 ? nextIdx : void 0;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
return errors;
|
|
236
|
+
}
|
|
237
|
+
|
|
67
238
|
// src/validate.ts
|
|
68
239
|
import { existsSync, realpathSync, statSync } from "fs";
|
|
69
240
|
import { isAbsolute, join, relative, resolve } from "path";
|
|
@@ -185,6 +356,37 @@ async function validateCourse(courseDir) {
|
|
|
185
356
|
severity: "error"
|
|
186
357
|
});
|
|
187
358
|
}
|
|
359
|
+
} else if (lesson.type === "component") {
|
|
360
|
+
if (!isBuiltinComponentId(lesson.component)) {
|
|
361
|
+
const resolved = resolveCoursePath(
|
|
362
|
+
resolvedDir,
|
|
363
|
+
join("components", lesson.component)
|
|
364
|
+
);
|
|
365
|
+
if (!resolved.ok) {
|
|
366
|
+
issues.push({
|
|
367
|
+
path: `lessons.${lesson.id}.component`,
|
|
368
|
+
message: resolved.message,
|
|
369
|
+
severity: "error"
|
|
370
|
+
});
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (!existsSync(resolved.path)) {
|
|
374
|
+
issues.push({
|
|
375
|
+
path: `lessons.${lesson.id}.component`,
|
|
376
|
+
message: `Unknown component "${lesson.component}" and no override at components/${lesson.component}`,
|
|
377
|
+
severity: "error"
|
|
378
|
+
});
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const contained = assertResolvedPathContained(resolvedDir, resolved.path);
|
|
382
|
+
if (!contained.ok) {
|
|
383
|
+
issues.push({
|
|
384
|
+
path: `lessons.${lesson.id}.component`,
|
|
385
|
+
message: contained.message,
|
|
386
|
+
severity: "error"
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
188
390
|
} else if (lesson.type === "html") {
|
|
189
391
|
const resolved = resolveCoursePath(resolvedDir, lesson.path);
|
|
190
392
|
if (!resolved.ok) {
|
|
@@ -339,6 +541,33 @@ async function validateCourse(courseDir) {
|
|
|
339
541
|
});
|
|
340
542
|
}
|
|
341
543
|
}
|
|
544
|
+
issues.push(...validateFlow(manifest));
|
|
545
|
+
if (manifest.variables) {
|
|
546
|
+
for (const [name, def] of Object.entries(manifest.variables)) {
|
|
547
|
+
const t = def.type;
|
|
548
|
+
if (t === "string" && typeof def.default !== "string") {
|
|
549
|
+
issues.push({
|
|
550
|
+
path: `variables.${name}.default`,
|
|
551
|
+
message: "Default must be a string when type is string",
|
|
552
|
+
severity: "error"
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
if (t === "number" && typeof def.default !== "number") {
|
|
556
|
+
issues.push({
|
|
557
|
+
path: `variables.${name}.default`,
|
|
558
|
+
message: "Default must be a number when type is number",
|
|
559
|
+
severity: "error"
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
if (t === "boolean" && typeof def.default !== "boolean") {
|
|
563
|
+
issues.push({
|
|
564
|
+
path: `variables.${name}.default`,
|
|
565
|
+
message: "Default must be a boolean when type is boolean",
|
|
566
|
+
severity: "error"
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
342
571
|
return {
|
|
343
572
|
valid: issues.filter((i) => i.severity === "error").length === 0,
|
|
344
573
|
manifest,
|
|
@@ -351,11 +580,15 @@ import { readFile as readFile2 } from "fs/promises";
|
|
|
351
580
|
import { parse as parseYaml2 } from "yaml";
|
|
352
581
|
function toLearnerAssessment(assessment) {
|
|
353
582
|
const answerKey = {};
|
|
583
|
+
const feedback = {};
|
|
354
584
|
const questions = assessment.questions.map((q) => {
|
|
355
585
|
const correct = q.choices.find((c) => c.correct === true);
|
|
356
586
|
if (correct) {
|
|
357
587
|
answerKey[q.id] = correct.id;
|
|
358
588
|
}
|
|
589
|
+
if (q.explanation) {
|
|
590
|
+
feedback[q.id] = q.explanation;
|
|
591
|
+
}
|
|
359
592
|
return {
|
|
360
593
|
id: q.id,
|
|
361
594
|
prompt: q.prompt,
|
|
@@ -369,12 +602,20 @@ function toLearnerAssessment(assessment) {
|
|
|
369
602
|
passingScore: assessment.passingScore,
|
|
370
603
|
questions
|
|
371
604
|
},
|
|
372
|
-
answerKey
|
|
605
|
+
answerKey,
|
|
606
|
+
config: {
|
|
607
|
+
maxAttempts: assessment.maxAttempts ?? 1,
|
|
608
|
+
shuffleChoices: assessment.shuffleChoices ?? false,
|
|
609
|
+
showFeedback: assessment.showFeedback ?? "never"
|
|
610
|
+
},
|
|
611
|
+
feedback
|
|
373
612
|
};
|
|
374
613
|
}
|
|
375
614
|
async function buildRuntimeAssessmentBundle(courseDir, manifest) {
|
|
376
615
|
const assessments = {};
|
|
377
616
|
const answerKeys = {};
|
|
617
|
+
const configs = {};
|
|
618
|
+
const feedback = {};
|
|
378
619
|
for (const ref of manifest.assessments ?? []) {
|
|
379
620
|
const resolved = resolveCoursePath(courseDir, ref.file);
|
|
380
621
|
if (!resolved.ok) {
|
|
@@ -393,29 +634,41 @@ async function buildRuntimeAssessmentBundle(courseDir, manifest) {
|
|
|
393
634
|
`Assessment file id "${parsed.data.id}" does not match manifest ref "${ref.id}"`
|
|
394
635
|
);
|
|
395
636
|
}
|
|
396
|
-
const
|
|
397
|
-
assessments[ref.id] = learner;
|
|
398
|
-
answerKeys[ref.id] = answerKey;
|
|
637
|
+
const built = toLearnerAssessment(parsed.data);
|
|
638
|
+
assessments[ref.id] = built.learner;
|
|
639
|
+
answerKeys[ref.id] = built.answerKey;
|
|
640
|
+
configs[ref.id] = built.config;
|
|
641
|
+
feedback[ref.id] = built.feedback;
|
|
399
642
|
}
|
|
400
|
-
return { assessments, answerKeys };
|
|
643
|
+
return { assessments, answerKeys, configs, feedback };
|
|
401
644
|
}
|
|
402
645
|
export {
|
|
646
|
+
BUILTIN_COMPONENT_IDS,
|
|
403
647
|
assertResolvedPathContained,
|
|
404
648
|
assessmentQuestionSchema,
|
|
405
649
|
assessmentRefSchema,
|
|
406
650
|
assessmentSchema,
|
|
407
651
|
buildRuntimeAssessmentBundle,
|
|
652
|
+
collectActivityIds,
|
|
653
|
+
componentLessonSchema,
|
|
654
|
+
conditionSchema,
|
|
408
655
|
courseManifestSchema,
|
|
656
|
+
detectFlowCycles,
|
|
657
|
+
flowRuleSchema,
|
|
409
658
|
formatErrorMessage,
|
|
410
659
|
formatIssuePath,
|
|
411
660
|
htmlLessonSchema,
|
|
661
|
+
isBuiltinComponentId,
|
|
412
662
|
isPathContained,
|
|
413
663
|
lessonSchema,
|
|
414
664
|
loadManifest,
|
|
415
665
|
markdownLessonSchema,
|
|
416
666
|
resolveCoursePath,
|
|
417
667
|
runtimeConfigSchema,
|
|
668
|
+
showFeedbackSchema,
|
|
418
669
|
toLearnerAssessment,
|
|
419
670
|
trackingSchema,
|
|
420
|
-
validateCourse
|
|
671
|
+
validateCourse,
|
|
672
|
+
validateFlow,
|
|
673
|
+
variableDefSchema
|
|
421
674
|
};
|