@lxpack/validators 0.1.0 → 0.1.1
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 +127 -0
- package/dist/index.d.ts +37 -5
- package/dist/index.js +150 -20
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# @lxpack/validators
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@lxpack/validators)
|
|
4
|
+
[](https://github.com/eddiethedean/lxpack/actions/workflows/ci.yml)
|
|
5
|
+
[](https://github.com/eddiethedean/lxpack/blob/main/LICENSE)
|
|
6
|
+
[](https://nodejs.org/)
|
|
7
|
+
|
|
8
|
+
Zod schemas and filesystem validation for LXPack course manifests.
|
|
9
|
+
|
|
10
|
+
Part of [LXPack](https://github.com/eddiethedean/lxpack) — an AI-native learning experience compiler and runtime.
|
|
11
|
+
|
|
12
|
+
| Related | Package |
|
|
13
|
+
|---------|---------|
|
|
14
|
+
| CLI | [`@lxpack/cli`](../cli/README.md) |
|
|
15
|
+
| Packaging | [`@lxpack/scorm`](../scorm/README.md) |
|
|
16
|
+
| Runtime | [`@lxpack/runtime`](../runtime/README.md) |
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @lxpack/validators
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Requires Node.js 20+.
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import {
|
|
30
|
+
validateCourse,
|
|
31
|
+
loadManifest,
|
|
32
|
+
buildRuntimeAssessmentBundle,
|
|
33
|
+
courseManifestSchema,
|
|
34
|
+
type ValidationResult,
|
|
35
|
+
} from "@lxpack/validators";
|
|
36
|
+
|
|
37
|
+
const result: ValidationResult = await validateCourse("/path/to/my-course");
|
|
38
|
+
|
|
39
|
+
if (!result.valid) {
|
|
40
|
+
for (const issue of result.issues) {
|
|
41
|
+
console.error(`${issue.path}: ${issue.message}`);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
const { manifest } = result;
|
|
45
|
+
const bundle = await buildRuntimeAssessmentBundle("/path/to/my-course", manifest);
|
|
46
|
+
// bundle.assessments — learner-facing questions (no correct flags)
|
|
47
|
+
// bundle.answerKeys — scoring keys for the runtime (embedded at build time)
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Load and parse only
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
const loaded = await loadManifest("/path/to/my-course");
|
|
55
|
+
if (Array.isArray(loaded)) {
|
|
56
|
+
// validation issues
|
|
57
|
+
} else {
|
|
58
|
+
const { manifest } = loaded;
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Schema-only validation
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const parsed = courseManifestSchema.safeParse(manifestObject);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Safe path resolution
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { resolveCoursePath, isPathContained } from "@lxpack/validators";
|
|
72
|
+
|
|
73
|
+
const abs = resolveCoursePath(courseDir, "lessons/intro.md");
|
|
74
|
+
isPathContained(courseDir, abs); // true if inside course root
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Exports
|
|
78
|
+
|
|
79
|
+
| Export | Description |
|
|
80
|
+
|--------|-------------|
|
|
81
|
+
| `validateCourse(dir)` | Parse `course.yaml`, validate schema, check files, symlink containment |
|
|
82
|
+
| `loadManifest(courseDir)` | Load and parse `course.yaml` |
|
|
83
|
+
| `buildRuntimeAssessmentBundle(dir, manifest)` | Load assessments; split learner view vs answer keys |
|
|
84
|
+
| `toLearnerAssessment(assessment)` | Strip `correct` / `explanation` from one assessment |
|
|
85
|
+
| `resolveCoursePath(dir, relativePath)` | Resolve a path safely inside the course directory |
|
|
86
|
+
| `isPathContained(root, target)` | Whether `target` stays under `root` |
|
|
87
|
+
| `courseManifestSchema` | Zod schema for the full course manifest |
|
|
88
|
+
| `lessonSchema`, `assessmentSchema`, … | Strict sub-schemas for manifest sections |
|
|
89
|
+
| `CourseManifest`, `Lesson`, `Assessment`, `LearnerAssessment`, `RuntimeAssessmentBundle` | TypeScript types |
|
|
90
|
+
|
|
91
|
+
## What gets validated
|
|
92
|
+
|
|
93
|
+
- Manifest shape (lessons, assessments, tracking rules)
|
|
94
|
+
- Lesson types: `markdown` (`file`) and `html` (`path`)
|
|
95
|
+
- Assessment YAML: strict MCQ schemas (`correct` on exactly one choice per question)
|
|
96
|
+
- Duplicate lesson IDs
|
|
97
|
+
- Path containment — referenced files must stay inside the course directory (including via symlinks)
|
|
98
|
+
- On-disk assets: files exist and assessments paths are regular files
|
|
99
|
+
|
|
100
|
+
## Assessment packaging
|
|
101
|
+
|
|
102
|
+
Author assessments live as YAML under `assessments/` in the course repo. At build/preview time:
|
|
103
|
+
|
|
104
|
+
1. `buildRuntimeAssessmentBundle()` reads each assessment file.
|
|
105
|
+
2. **Learner payload** — questions and choices without `correct` or `explanation`.
|
|
106
|
+
3. **Answer keys** — `questionId → choiceId` map for scoring.
|
|
107
|
+
|
|
108
|
+
The CLI and [`@lxpack/scorm`](../scorm/README.md) embed both in the HTML config JSON. Exported ZIPs **do not** include `assessments/` files, so answer keys are not fetchable as static assets.
|
|
109
|
+
|
|
110
|
+
## Development
|
|
111
|
+
|
|
112
|
+
From the monorepo root:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
pnpm --filter @lxpack/validators build
|
|
116
|
+
pnpm --filter @lxpack/validators test
|
|
117
|
+
pnpm --filter @lxpack/validators typecheck
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Links
|
|
121
|
+
|
|
122
|
+
- [LXPack repository](https://github.com/eddiethedean/lxpack)
|
|
123
|
+
- [Changelog](https://github.com/eddiethedean/lxpack/blob/main/CHANGELOG.md)
|
|
124
|
+
|
|
125
|
+
## License
|
|
126
|
+
|
|
127
|
+
Apache-2.0
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ declare const assessmentQuestionSchema: z.ZodEffects<z.ZodObject<{
|
|
|
7
7
|
id: z.ZodString;
|
|
8
8
|
text: z.ZodString;
|
|
9
9
|
correct: z.ZodOptional<z.ZodBoolean>;
|
|
10
|
-
}, "
|
|
10
|
+
}, "strict", z.ZodTypeAny, {
|
|
11
11
|
id: string;
|
|
12
12
|
text: string;
|
|
13
13
|
correct?: boolean | undefined;
|
|
@@ -17,7 +17,7 @@ declare const assessmentQuestionSchema: z.ZodEffects<z.ZodObject<{
|
|
|
17
17
|
correct?: boolean | undefined;
|
|
18
18
|
}>, "many">;
|
|
19
19
|
explanation: z.ZodOptional<z.ZodString>;
|
|
20
|
-
}, "
|
|
20
|
+
}, "strict", z.ZodTypeAny, {
|
|
21
21
|
id: string;
|
|
22
22
|
prompt: string;
|
|
23
23
|
choices: {
|
|
@@ -128,7 +128,7 @@ declare const assessmentSchema: z.ZodObject<{
|
|
|
128
128
|
id: z.ZodString;
|
|
129
129
|
text: z.ZodString;
|
|
130
130
|
correct: z.ZodOptional<z.ZodBoolean>;
|
|
131
|
-
}, "
|
|
131
|
+
}, "strict", z.ZodTypeAny, {
|
|
132
132
|
id: string;
|
|
133
133
|
text: string;
|
|
134
134
|
correct?: boolean | undefined;
|
|
@@ -138,7 +138,7 @@ declare const assessmentSchema: z.ZodObject<{
|
|
|
138
138
|
correct?: boolean | undefined;
|
|
139
139
|
}>, "many">;
|
|
140
140
|
explanation: z.ZodOptional<z.ZodString>;
|
|
141
|
-
}, "
|
|
141
|
+
}, "strict", z.ZodTypeAny, {
|
|
142
142
|
id: string;
|
|
143
143
|
prompt: string;
|
|
144
144
|
choices: {
|
|
@@ -379,6 +379,7 @@ interface ValidationResult {
|
|
|
379
379
|
manifest?: CourseManifest;
|
|
380
380
|
issues: ValidationIssue[];
|
|
381
381
|
}
|
|
382
|
+
declare function isPathContained(rootDir: string, candidatePath: string): boolean;
|
|
382
383
|
declare function resolveCoursePath(courseDir: string, relativePath: string): {
|
|
383
384
|
ok: true;
|
|
384
385
|
path: string;
|
|
@@ -386,10 +387,41 @@ declare function resolveCoursePath(courseDir: string, relativePath: string): {
|
|
|
386
387
|
ok: false;
|
|
387
388
|
message: string;
|
|
388
389
|
};
|
|
390
|
+
declare function assertResolvedPathContained(courseDir: string, resolvedPath: string): {
|
|
391
|
+
ok: true;
|
|
392
|
+
} | {
|
|
393
|
+
ok: false;
|
|
394
|
+
message: string;
|
|
395
|
+
};
|
|
389
396
|
declare function loadManifest(courseDir: string): Promise<{
|
|
390
397
|
manifest: CourseManifest;
|
|
391
398
|
raw: unknown;
|
|
392
399
|
} | ValidationIssue[]>;
|
|
393
400
|
declare function validateCourse(courseDir: string): Promise<ValidationResult>;
|
|
394
401
|
|
|
395
|
-
|
|
402
|
+
interface LearnerChoice {
|
|
403
|
+
id: string;
|
|
404
|
+
text: string;
|
|
405
|
+
}
|
|
406
|
+
interface LearnerQuestion {
|
|
407
|
+
id: string;
|
|
408
|
+
prompt: string;
|
|
409
|
+
choices: LearnerChoice[];
|
|
410
|
+
}
|
|
411
|
+
interface LearnerAssessment {
|
|
412
|
+
id: string;
|
|
413
|
+
title?: string;
|
|
414
|
+
passingScore: number;
|
|
415
|
+
questions: LearnerQuestion[];
|
|
416
|
+
}
|
|
417
|
+
interface RuntimeAssessmentBundle {
|
|
418
|
+
assessments: Record<string, LearnerAssessment>;
|
|
419
|
+
answerKeys: Record<string, Record<string, string>>;
|
|
420
|
+
}
|
|
421
|
+
declare function toLearnerAssessment(assessment: Assessment): {
|
|
422
|
+
learner: LearnerAssessment;
|
|
423
|
+
answerKey: Record<string, string>;
|
|
424
|
+
};
|
|
425
|
+
declare function buildRuntimeAssessmentBundle(courseDir: string, manifest: CourseManifest): Promise<RuntimeAssessmentBundle>;
|
|
426
|
+
|
|
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 };
|
package/dist/index.js
CHANGED
|
@@ -4,13 +4,13 @@ var choiceSchema = z.object({
|
|
|
4
4
|
id: z.string().min(1),
|
|
5
5
|
text: z.string().min(1),
|
|
6
6
|
correct: z.boolean().optional()
|
|
7
|
-
});
|
|
7
|
+
}).strict();
|
|
8
8
|
var assessmentQuestionSchema = z.object({
|
|
9
9
|
id: z.string().min(1),
|
|
10
10
|
prompt: z.string().min(1),
|
|
11
11
|
choices: z.array(choiceSchema).min(1),
|
|
12
12
|
explanation: z.string().optional()
|
|
13
|
-
}).superRefine((question, ctx) => {
|
|
13
|
+
}).strict().superRefine((question, ctx) => {
|
|
14
14
|
const correctCount = question.choices.filter((c) => c.correct === true).length;
|
|
15
15
|
if (correctCount !== 1) {
|
|
16
16
|
ctx.addIssue({
|
|
@@ -65,8 +65,8 @@ var courseManifestSchema = z.object({
|
|
|
65
65
|
}).strict();
|
|
66
66
|
|
|
67
67
|
// src/validate.ts
|
|
68
|
-
import { existsSync, statSync } from "fs";
|
|
69
|
-
import { join,
|
|
68
|
+
import { existsSync, realpathSync, statSync } from "fs";
|
|
69
|
+
import { isAbsolute, join, relative, resolve } from "path";
|
|
70
70
|
import { parse as parseYaml } from "yaml";
|
|
71
71
|
import { readFile } from "fs/promises";
|
|
72
72
|
function formatErrorMessage(err) {
|
|
@@ -76,20 +76,39 @@ function formatIssuePath(path) {
|
|
|
76
76
|
const joined = path.map(String).join(".");
|
|
77
77
|
return joined || "course.yaml";
|
|
78
78
|
}
|
|
79
|
+
function isPathContained(rootDir, candidatePath) {
|
|
80
|
+
const root = resolve(rootDir);
|
|
81
|
+
const candidate = resolve(candidatePath);
|
|
82
|
+
const rel = relative(root, candidate);
|
|
83
|
+
if (rel === "") return true;
|
|
84
|
+
return !rel.startsWith("..") && !isAbsolute(rel);
|
|
85
|
+
}
|
|
79
86
|
function resolveCoursePath(courseDir, relativePath) {
|
|
80
87
|
if (relativePath.startsWith("/") || /^[a-zA-Z]:\\/.test(relativePath)) {
|
|
81
88
|
return { ok: false, message: "Absolute paths are not allowed" };
|
|
82
89
|
}
|
|
83
90
|
const resolvedDir = resolve(courseDir);
|
|
84
91
|
const resolvedPath = resolve(resolvedDir, relativePath);
|
|
85
|
-
|
|
86
|
-
if (!resolvedPath.startsWith(prefix)) {
|
|
92
|
+
if (!isPathContained(resolvedDir, resolvedPath)) {
|
|
87
93
|
return { ok: false, message: "Path escapes course directory" };
|
|
88
94
|
}
|
|
89
95
|
return { ok: true, path: resolvedPath };
|
|
90
96
|
}
|
|
97
|
+
function assertResolvedPathContained(courseDir, resolvedPath) {
|
|
98
|
+
try {
|
|
99
|
+
const root = realpathSync(resolve(courseDir));
|
|
100
|
+
const target = realpathSync(resolvedPath);
|
|
101
|
+
if (!isPathContained(root, target)) {
|
|
102
|
+
return { ok: false, message: "Path escapes course directory" };
|
|
103
|
+
}
|
|
104
|
+
return { ok: true };
|
|
105
|
+
} catch {
|
|
106
|
+
return { ok: false, message: "Path could not be resolved" };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
91
109
|
async function loadManifest(courseDir) {
|
|
92
|
-
const
|
|
110
|
+
const resolvedDir = resolve(courseDir);
|
|
111
|
+
const manifestPath = join(resolvedDir, "course.yaml");
|
|
93
112
|
if (!existsSync(manifestPath)) {
|
|
94
113
|
return [
|
|
95
114
|
{
|
|
@@ -149,6 +168,15 @@ async function validateCourse(courseDir) {
|
|
|
149
168
|
});
|
|
150
169
|
continue;
|
|
151
170
|
}
|
|
171
|
+
const contained = assertResolvedPathContained(resolvedDir, resolved.path);
|
|
172
|
+
if (!contained.ok) {
|
|
173
|
+
issues.push({
|
|
174
|
+
path: `lessons.${lesson.id}.file`,
|
|
175
|
+
message: contained.message,
|
|
176
|
+
severity: "error"
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
152
180
|
const stat = statSync(resolved.path);
|
|
153
181
|
if (!stat.isFile()) {
|
|
154
182
|
issues.push({
|
|
@@ -175,6 +203,15 @@ async function validateCourse(courseDir) {
|
|
|
175
203
|
});
|
|
176
204
|
continue;
|
|
177
205
|
}
|
|
206
|
+
const contained = assertResolvedPathContained(resolvedDir, resolved.path);
|
|
207
|
+
if (!contained.ok) {
|
|
208
|
+
issues.push({
|
|
209
|
+
path: `lessons.${lesson.id}.path`,
|
|
210
|
+
message: contained.message,
|
|
211
|
+
severity: "error"
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
178
215
|
const stat = statSync(resolved.path);
|
|
179
216
|
if (!stat.isDirectory()) {
|
|
180
217
|
issues.push({
|
|
@@ -191,12 +228,24 @@ async function validateCourse(courseDir) {
|
|
|
191
228
|
message: `HTML interaction missing index.html: ${lesson.path}`,
|
|
192
229
|
severity: "error"
|
|
193
230
|
});
|
|
194
|
-
} else
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
231
|
+
} else {
|
|
232
|
+
const indexContained = assertResolvedPathContained(
|
|
233
|
+
resolvedDir,
|
|
234
|
+
indexPath
|
|
235
|
+
);
|
|
236
|
+
if (!indexContained.ok) {
|
|
237
|
+
issues.push({
|
|
238
|
+
path: `lessons.${lesson.id}.path`,
|
|
239
|
+
message: indexContained.message,
|
|
240
|
+
severity: "error"
|
|
241
|
+
});
|
|
242
|
+
} else if (!statSync(indexPath).isFile()) {
|
|
243
|
+
issues.push({
|
|
244
|
+
path: `lessons.${lesson.id}.path`,
|
|
245
|
+
message: `index.html is not a file: ${lesson.path}/index.html`,
|
|
246
|
+
severity: "error"
|
|
247
|
+
});
|
|
248
|
+
}
|
|
200
249
|
}
|
|
201
250
|
}
|
|
202
251
|
}
|
|
@@ -228,6 +277,24 @@ async function validateCourse(courseDir) {
|
|
|
228
277
|
});
|
|
229
278
|
continue;
|
|
230
279
|
}
|
|
280
|
+
const contained = assertResolvedPathContained(resolvedDir, resolved.path);
|
|
281
|
+
if (!contained.ok) {
|
|
282
|
+
issues.push({
|
|
283
|
+
path: `assessments.${ref.id}.file`,
|
|
284
|
+
message: contained.message,
|
|
285
|
+
severity: "error"
|
|
286
|
+
});
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const assessmentStat = statSync(resolved.path);
|
|
290
|
+
if (!assessmentStat.isFile()) {
|
|
291
|
+
issues.push({
|
|
292
|
+
path: `assessments.${ref.id}.file`,
|
|
293
|
+
message: `Assessment path is not a file: ${ref.file}`,
|
|
294
|
+
severity: "error"
|
|
295
|
+
});
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
231
298
|
try {
|
|
232
299
|
const content = await readFile(resolved.path, "utf-8");
|
|
233
300
|
const raw = parseYaml(content);
|
|
@@ -259,13 +326,18 @@ async function validateCourse(courseDir) {
|
|
|
259
326
|
}
|
|
260
327
|
}
|
|
261
328
|
}
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
329
|
+
const lessonIdCounts = /* @__PURE__ */ new Map();
|
|
330
|
+
for (const lesson of manifest.lessons) {
|
|
331
|
+
lessonIdCounts.set(lesson.id, (lessonIdCounts.get(lesson.id) ?? 0) + 1);
|
|
332
|
+
}
|
|
333
|
+
for (const [id, count] of lessonIdCounts) {
|
|
334
|
+
if (count > 1) {
|
|
335
|
+
issues.push({
|
|
336
|
+
path: "lessons",
|
|
337
|
+
message: `Duplicate lesson ID: ${id}`,
|
|
338
|
+
severity: "error"
|
|
339
|
+
});
|
|
340
|
+
}
|
|
269
341
|
}
|
|
270
342
|
return {
|
|
271
343
|
valid: issues.filter((i) => i.severity === "error").length === 0,
|
|
@@ -273,19 +345,77 @@ async function validateCourse(courseDir) {
|
|
|
273
345
|
issues
|
|
274
346
|
};
|
|
275
347
|
}
|
|
348
|
+
|
|
349
|
+
// src/assessments.ts
|
|
350
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
351
|
+
import { parse as parseYaml2 } from "yaml";
|
|
352
|
+
function toLearnerAssessment(assessment) {
|
|
353
|
+
const answerKey = {};
|
|
354
|
+
const questions = assessment.questions.map((q) => {
|
|
355
|
+
const correct = q.choices.find((c) => c.correct === true);
|
|
356
|
+
if (correct) {
|
|
357
|
+
answerKey[q.id] = correct.id;
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
id: q.id,
|
|
361
|
+
prompt: q.prompt,
|
|
362
|
+
choices: q.choices.map((c) => ({ id: c.id, text: c.text }))
|
|
363
|
+
};
|
|
364
|
+
});
|
|
365
|
+
return {
|
|
366
|
+
learner: {
|
|
367
|
+
id: assessment.id,
|
|
368
|
+
title: assessment.title,
|
|
369
|
+
passingScore: assessment.passingScore,
|
|
370
|
+
questions
|
|
371
|
+
},
|
|
372
|
+
answerKey
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
async function buildRuntimeAssessmentBundle(courseDir, manifest) {
|
|
376
|
+
const assessments = {};
|
|
377
|
+
const answerKeys = {};
|
|
378
|
+
for (const ref of manifest.assessments ?? []) {
|
|
379
|
+
const resolved = resolveCoursePath(courseDir, ref.file);
|
|
380
|
+
if (!resolved.ok) {
|
|
381
|
+
throw new Error(resolved.message);
|
|
382
|
+
}
|
|
383
|
+
const content = await readFile2(resolved.path, "utf-8");
|
|
384
|
+
const raw = parseYaml2(content);
|
|
385
|
+
const parsed = assessmentSchema.safeParse(raw);
|
|
386
|
+
if (!parsed.success) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`Invalid assessment ${ref.file}: ${parsed.error.issues.map((i) => i.message).join("; ")}`
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
if (parsed.data.id !== ref.id) {
|
|
392
|
+
throw new Error(
|
|
393
|
+
`Assessment file id "${parsed.data.id}" does not match manifest ref "${ref.id}"`
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
const { learner, answerKey } = toLearnerAssessment(parsed.data);
|
|
397
|
+
assessments[ref.id] = learner;
|
|
398
|
+
answerKeys[ref.id] = answerKey;
|
|
399
|
+
}
|
|
400
|
+
return { assessments, answerKeys };
|
|
401
|
+
}
|
|
276
402
|
export {
|
|
403
|
+
assertResolvedPathContained,
|
|
277
404
|
assessmentQuestionSchema,
|
|
278
405
|
assessmentRefSchema,
|
|
279
406
|
assessmentSchema,
|
|
407
|
+
buildRuntimeAssessmentBundle,
|
|
280
408
|
courseManifestSchema,
|
|
281
409
|
formatErrorMessage,
|
|
282
410
|
formatIssuePath,
|
|
283
411
|
htmlLessonSchema,
|
|
412
|
+
isPathContained,
|
|
284
413
|
lessonSchema,
|
|
285
414
|
loadManifest,
|
|
286
415
|
markdownLessonSchema,
|
|
287
416
|
resolveCoursePath,
|
|
288
417
|
runtimeConfigSchema,
|
|
418
|
+
toLearnerAssessment,
|
|
289
419
|
trackingSchema,
|
|
290
420
|
validateCourse
|
|
291
421
|
};
|