@lxpack/validators 0.1.1 → 0.2.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 +35 -13
- package/dist/index.d.ts +240 -29
- package/dist/index.js +655 -292
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,81 +1,285 @@
|
|
|
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)
|
|
44
92
|
}).strict();
|
|
45
|
-
var assessmentRefSchema =
|
|
46
|
-
id:
|
|
47
|
-
file:
|
|
93
|
+
var assessmentRefSchema = z2.object({
|
|
94
|
+
id: z2.string().min(1),
|
|
95
|
+
file: z2.string().min(1)
|
|
48
96
|
}).strict();
|
|
49
|
-
var
|
|
50
|
-
|
|
51
|
-
|
|
97
|
+
var variableDefSchema = z2.object({
|
|
98
|
+
default: z2.union([z2.string(), z2.number(), z2.boolean()]),
|
|
99
|
+
type: z2.enum(["string", "number", "boolean"]).optional()
|
|
100
|
+
}).strict().superRefine((def, ctx) => {
|
|
101
|
+
const t = def.type;
|
|
102
|
+
if (t === "string" && typeof def.default !== "string") {
|
|
103
|
+
ctx.addIssue({
|
|
104
|
+
code: z2.ZodIssueCode.custom,
|
|
105
|
+
message: "Default must be a string when type is string",
|
|
106
|
+
path: ["default"]
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
if (t === "number" && typeof def.default !== "number") {
|
|
110
|
+
ctx.addIssue({
|
|
111
|
+
code: z2.ZodIssueCode.custom,
|
|
112
|
+
message: "Default must be a number when type is number",
|
|
113
|
+
path: ["default"]
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
if (t === "boolean" && typeof def.default !== "boolean") {
|
|
117
|
+
ctx.addIssue({
|
|
118
|
+
code: z2.ZodIssueCode.custom,
|
|
119
|
+
message: "Default must be a boolean when type is boolean",
|
|
120
|
+
path: ["default"]
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
var trackingSchema = z2.object({
|
|
125
|
+
completion: z2.object({
|
|
126
|
+
threshold: z2.number().min(0).max(1).default(0.9)
|
|
52
127
|
}).strict().optional()
|
|
53
128
|
}).strict().optional();
|
|
54
|
-
var runtimeConfigSchema =
|
|
55
|
-
theme:
|
|
129
|
+
var runtimeConfigSchema = z2.object({
|
|
130
|
+
theme: z2.string().default("modern")
|
|
56
131
|
}).strict().optional();
|
|
57
|
-
var courseManifestSchema =
|
|
58
|
-
title:
|
|
59
|
-
version:
|
|
60
|
-
description:
|
|
132
|
+
var courseManifestSchema = z2.object({
|
|
133
|
+
title: z2.string().min(1),
|
|
134
|
+
version: z2.string().min(1),
|
|
135
|
+
description: z2.string().optional(),
|
|
61
136
|
runtime: runtimeConfigSchema,
|
|
62
137
|
tracking: trackingSchema,
|
|
63
|
-
|
|
64
|
-
|
|
138
|
+
variables: z2.record(variableDefSchema).optional(),
|
|
139
|
+
flow: z2.array(flowRuleSchema).optional(),
|
|
140
|
+
lessons: z2.array(lessonSchema).min(1),
|
|
141
|
+
assessments: z2.array(assessmentRefSchema).optional()
|
|
65
142
|
}).strict();
|
|
66
143
|
|
|
67
|
-
// src/
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
144
|
+
// src/components.ts
|
|
145
|
+
var BUILTIN_COMPONENT_IDS = [
|
|
146
|
+
"callout",
|
|
147
|
+
"image-card",
|
|
148
|
+
"checklist"
|
|
149
|
+
];
|
|
150
|
+
function isBuiltinComponentId(id) {
|
|
151
|
+
return BUILTIN_COMPONENT_IDS.includes(id);
|
|
74
152
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
153
|
+
|
|
154
|
+
// src/flow-validate.ts
|
|
155
|
+
function collectActivityIds(manifest) {
|
|
156
|
+
const ids = /* @__PURE__ */ new Set();
|
|
157
|
+
for (const lesson of manifest.lessons) {
|
|
158
|
+
ids.add(lesson.id);
|
|
159
|
+
}
|
|
160
|
+
for (const ref of manifest.assessments ?? []) {
|
|
161
|
+
ids.add(ref.id);
|
|
162
|
+
}
|
|
163
|
+
return ids;
|
|
164
|
+
}
|
|
165
|
+
function collectAssessmentIds(manifest) {
|
|
166
|
+
const ids = /* @__PURE__ */ new Set();
|
|
167
|
+
for (const ref of manifest.assessments ?? []) {
|
|
168
|
+
ids.add(ref.id);
|
|
169
|
+
}
|
|
170
|
+
return ids;
|
|
171
|
+
}
|
|
172
|
+
function collectConditionRefs(condition, refs) {
|
|
173
|
+
if ("variable" in condition && condition.variable?.eq) {
|
|
174
|
+
refs.variables.add(condition.variable.eq[0]);
|
|
175
|
+
}
|
|
176
|
+
if ("assessment" in condition && condition.assessment?.passed) {
|
|
177
|
+
refs.assessments.add(condition.assessment.passed);
|
|
178
|
+
}
|
|
179
|
+
if ("interaction" in condition && condition.interaction?.done) {
|
|
180
|
+
refs.interactions.add(condition.interaction.done);
|
|
181
|
+
}
|
|
182
|
+
if ("all" in condition && condition.all) {
|
|
183
|
+
for (const c of condition.all) collectConditionRefs(c, refs);
|
|
184
|
+
}
|
|
185
|
+
if ("any" in condition && condition.any) {
|
|
186
|
+
for (const c of condition.any) collectConditionRefs(c, refs);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function validateFlow(manifest) {
|
|
190
|
+
const issues = [];
|
|
191
|
+
const flow = manifest.flow;
|
|
192
|
+
if (!flow?.length) return issues;
|
|
193
|
+
const activityIds = collectActivityIds(manifest);
|
|
194
|
+
const assessmentIds = collectAssessmentIds(manifest);
|
|
195
|
+
const manifestVars = new Set(Object.keys(manifest.variables ?? {}));
|
|
196
|
+
flow.forEach((rule, index) => {
|
|
197
|
+
const path = `flow[${index}]`;
|
|
198
|
+
if (!activityIds.has(rule.goto)) {
|
|
199
|
+
issues.push({
|
|
200
|
+
path: `${path}.goto`,
|
|
201
|
+
message: `Unknown activity id: ${rule.goto}`,
|
|
202
|
+
severity: "error"
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const refs = {
|
|
206
|
+
variables: /* @__PURE__ */ new Set(),
|
|
207
|
+
assessments: /* @__PURE__ */ new Set(),
|
|
208
|
+
interactions: /* @__PURE__ */ new Set()
|
|
209
|
+
};
|
|
210
|
+
collectConditionRefs(rule.when, refs);
|
|
211
|
+
for (const v of refs.variables) {
|
|
212
|
+
if (!manifestVars.has(v)) {
|
|
213
|
+
issues.push({
|
|
214
|
+
path: `${path}.when`,
|
|
215
|
+
message: `Unknown variable in condition: ${v}`,
|
|
216
|
+
severity: "error"
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const a of refs.assessments) {
|
|
221
|
+
if (!assessmentIds.has(a)) {
|
|
222
|
+
issues.push({
|
|
223
|
+
path: `${path}.when`,
|
|
224
|
+
message: `Unknown assessment in condition: ${a}`,
|
|
225
|
+
severity: "error"
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
for (const i of refs.interactions) {
|
|
230
|
+
if (!activityIds.has(i)) {
|
|
231
|
+
issues.push({
|
|
232
|
+
path: `${path}.when`,
|
|
233
|
+
message: `Unknown interaction/lesson id in condition: ${i}`,
|
|
234
|
+
severity: "error"
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
return issues;
|
|
240
|
+
}
|
|
241
|
+
function detectFlowCycles(flow) {
|
|
242
|
+
const gotoOf = /* @__PURE__ */ new Map();
|
|
243
|
+
flow.forEach((rule, i) => gotoOf.set(i, rule.goto));
|
|
244
|
+
const errors = [];
|
|
245
|
+
const visited = /* @__PURE__ */ new Set();
|
|
246
|
+
for (let start = 0; start < flow.length; start++) {
|
|
247
|
+
if (visited.has(start)) continue;
|
|
248
|
+
const chain = /* @__PURE__ */ new Set();
|
|
249
|
+
let i = start;
|
|
250
|
+
while (i !== void 0 && i < flow.length) {
|
|
251
|
+
const ruleIndex = i;
|
|
252
|
+
if (chain.has(ruleIndex)) {
|
|
253
|
+
errors.push(`Flow rule cycle detected involving flow[${ruleIndex}]`);
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
if (visited.has(ruleIndex)) break;
|
|
257
|
+
chain.add(ruleIndex);
|
|
258
|
+
visited.add(ruleIndex);
|
|
259
|
+
const target = gotoOf.get(ruleIndex);
|
|
260
|
+
const nextIdx = flow.findIndex(
|
|
261
|
+
(_, idx) => idx > ruleIndex && flow[idx].goto === target
|
|
262
|
+
);
|
|
263
|
+
i = nextIdx >= 0 ? nextIdx : void 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return errors;
|
|
78
267
|
}
|
|
268
|
+
|
|
269
|
+
// src/validate.ts
|
|
270
|
+
import { existsSync as existsSync5 } from "fs";
|
|
271
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
272
|
+
import { join as join3, resolve as resolve2 } from "path";
|
|
273
|
+
import { parse as parseYaml2 } from "yaml";
|
|
274
|
+
|
|
275
|
+
// src/course-assessments.ts
|
|
276
|
+
import { existsSync, statSync } from "fs";
|
|
277
|
+
import { readFile } from "fs/promises";
|
|
278
|
+
import { parse as parseYaml } from "yaml";
|
|
279
|
+
|
|
280
|
+
// src/course-paths.ts
|
|
281
|
+
import { realpathSync } from "fs";
|
|
282
|
+
import { isAbsolute, relative, resolve } from "path";
|
|
79
283
|
function isPathContained(rootDir, candidatePath) {
|
|
80
284
|
const root = resolve(rootDir);
|
|
81
285
|
const candidate = resolve(candidatePath);
|
|
@@ -106,10 +310,359 @@ function assertResolvedPathContained(courseDir, resolvedPath) {
|
|
|
106
310
|
return { ok: false, message: "Path could not be resolved" };
|
|
107
311
|
}
|
|
108
312
|
}
|
|
313
|
+
|
|
314
|
+
// src/assessments.ts
|
|
315
|
+
function toLearnerAssessment(assessment) {
|
|
316
|
+
const answerKey = {};
|
|
317
|
+
const feedback = {};
|
|
318
|
+
const questions = assessment.questions.map((q) => {
|
|
319
|
+
const correct = q.choices.find((c) => c.correct === true);
|
|
320
|
+
if (correct) {
|
|
321
|
+
answerKey[q.id] = correct.id;
|
|
322
|
+
}
|
|
323
|
+
if (q.explanation) {
|
|
324
|
+
feedback[q.id] = q.explanation;
|
|
325
|
+
}
|
|
326
|
+
return {
|
|
327
|
+
id: q.id,
|
|
328
|
+
prompt: q.prompt,
|
|
329
|
+
choices: q.choices.map((c) => ({ id: c.id, text: c.text }))
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
return {
|
|
333
|
+
learner: {
|
|
334
|
+
id: assessment.id,
|
|
335
|
+
title: assessment.title,
|
|
336
|
+
passingScore: assessment.passingScore,
|
|
337
|
+
questions
|
|
338
|
+
},
|
|
339
|
+
answerKey,
|
|
340
|
+
config: {
|
|
341
|
+
maxAttempts: assessment.maxAttempts ?? 1,
|
|
342
|
+
shuffleChoices: assessment.shuffleChoices ?? false,
|
|
343
|
+
showFeedback: assessment.showFeedback ?? "never"
|
|
344
|
+
},
|
|
345
|
+
feedback
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// src/course-assessments.ts
|
|
350
|
+
async function loadParsedAssessments(courseDir, manifest) {
|
|
351
|
+
const resolvedDir = courseDir;
|
|
352
|
+
const issues = [];
|
|
353
|
+
const parsed = /* @__PURE__ */ new Map();
|
|
354
|
+
const assessmentIds = /* @__PURE__ */ new Set();
|
|
355
|
+
for (const ref of manifest.assessments ?? []) {
|
|
356
|
+
if (assessmentIds.has(ref.id)) {
|
|
357
|
+
issues.push({
|
|
358
|
+
path: "assessments",
|
|
359
|
+
message: `Duplicate assessment ID: ${ref.id}`,
|
|
360
|
+
severity: "error"
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
assessmentIds.add(ref.id);
|
|
364
|
+
const resolved = resolveCoursePath(resolvedDir, ref.file);
|
|
365
|
+
if (!resolved.ok) {
|
|
366
|
+
issues.push({
|
|
367
|
+
path: `assessments.${ref.id}.file`,
|
|
368
|
+
message: resolved.message,
|
|
369
|
+
severity: "error"
|
|
370
|
+
});
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (!existsSync(resolved.path)) {
|
|
374
|
+
issues.push({
|
|
375
|
+
path: `assessments.${ref.id}.file`,
|
|
376
|
+
message: `Assessment file not found: ${ref.file}`,
|
|
377
|
+
severity: "error"
|
|
378
|
+
});
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const contained = assertResolvedPathContained(resolvedDir, resolved.path);
|
|
382
|
+
if (!contained.ok) {
|
|
383
|
+
issues.push({
|
|
384
|
+
path: `assessments.${ref.id}.file`,
|
|
385
|
+
message: contained.message,
|
|
386
|
+
severity: "error"
|
|
387
|
+
});
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
const assessmentStat = statSync(resolved.path);
|
|
391
|
+
if (!assessmentStat.isFile()) {
|
|
392
|
+
issues.push({
|
|
393
|
+
path: `assessments.${ref.id}.file`,
|
|
394
|
+
message: `Assessment path is not a file: ${ref.file}`,
|
|
395
|
+
severity: "error"
|
|
396
|
+
});
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
const content = await readFile(resolved.path, "utf-8");
|
|
401
|
+
const raw = parseYaml(content);
|
|
402
|
+
const result = assessmentSchema.safeParse(raw);
|
|
403
|
+
if (!result.success) {
|
|
404
|
+
for (const issue of result.error.issues) {
|
|
405
|
+
const subPath = issue.path.length ? issue.path.join(".") : "root";
|
|
406
|
+
issues.push({
|
|
407
|
+
path: `${ref.file}:${subPath}`,
|
|
408
|
+
message: issue.message,
|
|
409
|
+
severity: "error"
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (result.data.id !== ref.id) {
|
|
415
|
+
issues.push({
|
|
416
|
+
path: `assessments.${ref.id}`,
|
|
417
|
+
message: `Assessment file id "${result.data.id}" does not match manifest ref id "${ref.id}"`,
|
|
418
|
+
severity: "error"
|
|
419
|
+
});
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
parsed.set(ref.id, result.data);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
issues.push({
|
|
425
|
+
path: ref.file,
|
|
426
|
+
message: `Failed to parse assessment: ${formatErrorMessage(err)}`,
|
|
427
|
+
severity: "error"
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return { parsed, issues };
|
|
432
|
+
}
|
|
433
|
+
function buildRuntimeAssessmentBundleFromParsed(parsed) {
|
|
434
|
+
const assessments = {};
|
|
435
|
+
const answerKeys = {};
|
|
436
|
+
const configs = {};
|
|
437
|
+
const feedback = {};
|
|
438
|
+
for (const [id, assessment] of parsed) {
|
|
439
|
+
const built = toLearnerAssessment(assessment);
|
|
440
|
+
assessments[id] = built.learner;
|
|
441
|
+
answerKeys[id] = built.answerKey;
|
|
442
|
+
configs[id] = built.config;
|
|
443
|
+
feedback[id] = built.feedback;
|
|
444
|
+
}
|
|
445
|
+
return { assessments, answerKeys, configs, feedback };
|
|
446
|
+
}
|
|
447
|
+
async function buildRuntimeAssessmentBundle(courseDir, manifest) {
|
|
448
|
+
const { parsed, issues } = await loadParsedAssessments(courseDir, manifest);
|
|
449
|
+
const errors = issues.filter((i) => i.severity === "error");
|
|
450
|
+
if (errors.length > 0) {
|
|
451
|
+
throw new Error(
|
|
452
|
+
errors.map((i) => `${i.path}: ${i.message}`).join("; ")
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
return buildRuntimeAssessmentBundleFromParsed(parsed);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/validate/lesson-markdown.ts
|
|
459
|
+
import { existsSync as existsSync2, statSync as statSync2 } from "fs";
|
|
460
|
+
function validateMarkdownLesson(courseDir, lesson) {
|
|
461
|
+
const issues = [];
|
|
462
|
+
const resolved = resolveCoursePath(courseDir, lesson.file);
|
|
463
|
+
if (!resolved.ok) {
|
|
464
|
+
issues.push({
|
|
465
|
+
path: `lessons.${lesson.id}.file`,
|
|
466
|
+
message: resolved.message,
|
|
467
|
+
severity: "error"
|
|
468
|
+
});
|
|
469
|
+
return issues;
|
|
470
|
+
}
|
|
471
|
+
if (!existsSync2(resolved.path)) {
|
|
472
|
+
issues.push({
|
|
473
|
+
path: `lessons.${lesson.id}.file`,
|
|
474
|
+
message: `Lesson file not found: ${lesson.file}`,
|
|
475
|
+
severity: "error"
|
|
476
|
+
});
|
|
477
|
+
return issues;
|
|
478
|
+
}
|
|
479
|
+
const contained = assertResolvedPathContained(courseDir, resolved.path);
|
|
480
|
+
if (!contained.ok) {
|
|
481
|
+
issues.push({
|
|
482
|
+
path: `lessons.${lesson.id}.file`,
|
|
483
|
+
message: contained.message,
|
|
484
|
+
severity: "error"
|
|
485
|
+
});
|
|
486
|
+
return issues;
|
|
487
|
+
}
|
|
488
|
+
const stat = statSync2(resolved.path);
|
|
489
|
+
if (!stat.isFile()) {
|
|
490
|
+
issues.push({
|
|
491
|
+
path: `lessons.${lesson.id}.file`,
|
|
492
|
+
message: `Lesson path is not a file: ${lesson.file}`,
|
|
493
|
+
severity: "error"
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
return issues;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// src/validate/lesson-html.ts
|
|
500
|
+
import { existsSync as existsSync3, statSync as statSync3 } from "fs";
|
|
501
|
+
import { join } from "path";
|
|
502
|
+
function validateHtmlLesson(courseDir, lesson) {
|
|
503
|
+
const issues = [];
|
|
504
|
+
const resolved = resolveCoursePath(courseDir, lesson.path);
|
|
505
|
+
if (!resolved.ok) {
|
|
506
|
+
issues.push({
|
|
507
|
+
path: `lessons.${lesson.id}.path`,
|
|
508
|
+
message: resolved.message,
|
|
509
|
+
severity: "error"
|
|
510
|
+
});
|
|
511
|
+
return issues;
|
|
512
|
+
}
|
|
513
|
+
if (!existsSync3(resolved.path)) {
|
|
514
|
+
issues.push({
|
|
515
|
+
path: `lessons.${lesson.id}.path`,
|
|
516
|
+
message: `HTML interaction directory not found: ${lesson.path}`,
|
|
517
|
+
severity: "error"
|
|
518
|
+
});
|
|
519
|
+
return issues;
|
|
520
|
+
}
|
|
521
|
+
const contained = assertResolvedPathContained(courseDir, resolved.path);
|
|
522
|
+
if (!contained.ok) {
|
|
523
|
+
issues.push({
|
|
524
|
+
path: `lessons.${lesson.id}.path`,
|
|
525
|
+
message: contained.message,
|
|
526
|
+
severity: "error"
|
|
527
|
+
});
|
|
528
|
+
return issues;
|
|
529
|
+
}
|
|
530
|
+
const stat = statSync3(resolved.path);
|
|
531
|
+
if (!stat.isDirectory()) {
|
|
532
|
+
issues.push({
|
|
533
|
+
path: `lessons.${lesson.id}.path`,
|
|
534
|
+
message: `HTML interaction path is not a directory: ${lesson.path}`,
|
|
535
|
+
severity: "error"
|
|
536
|
+
});
|
|
537
|
+
return issues;
|
|
538
|
+
}
|
|
539
|
+
const indexPath = join(resolved.path, "index.html");
|
|
540
|
+
if (!existsSync3(indexPath)) {
|
|
541
|
+
issues.push({
|
|
542
|
+
path: `lessons.${lesson.id}.path`,
|
|
543
|
+
message: `HTML interaction missing index.html: ${lesson.path}`,
|
|
544
|
+
severity: "error"
|
|
545
|
+
});
|
|
546
|
+
return issues;
|
|
547
|
+
}
|
|
548
|
+
const indexContained = assertResolvedPathContained(courseDir, indexPath);
|
|
549
|
+
if (!indexContained.ok) {
|
|
550
|
+
issues.push({
|
|
551
|
+
path: `lessons.${lesson.id}.path`,
|
|
552
|
+
message: indexContained.message,
|
|
553
|
+
severity: "error"
|
|
554
|
+
});
|
|
555
|
+
return issues;
|
|
556
|
+
}
|
|
557
|
+
if (!statSync3(indexPath).isFile()) {
|
|
558
|
+
issues.push({
|
|
559
|
+
path: `lessons.${lesson.id}.path`,
|
|
560
|
+
message: `index.html is not a file: ${lesson.path}/index.html`,
|
|
561
|
+
severity: "error"
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
return issues;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/validate/lesson-component.ts
|
|
568
|
+
import { existsSync as existsSync4, statSync as statSync4 } from "fs";
|
|
569
|
+
import { join as join2 } from "path";
|
|
570
|
+
function validateComponentLesson(courseDir, lesson) {
|
|
571
|
+
const issues = [];
|
|
572
|
+
if (isBuiltinComponentId(lesson.component)) {
|
|
573
|
+
return issues;
|
|
574
|
+
}
|
|
575
|
+
const resolved = resolveCoursePath(
|
|
576
|
+
courseDir,
|
|
577
|
+
join2("components", lesson.component)
|
|
578
|
+
);
|
|
579
|
+
if (!resolved.ok) {
|
|
580
|
+
issues.push({
|
|
581
|
+
path: `lessons.${lesson.id}.component`,
|
|
582
|
+
message: resolved.message,
|
|
583
|
+
severity: "error"
|
|
584
|
+
});
|
|
585
|
+
return issues;
|
|
586
|
+
}
|
|
587
|
+
if (!existsSync4(resolved.path)) {
|
|
588
|
+
issues.push({
|
|
589
|
+
path: `lessons.${lesson.id}.component`,
|
|
590
|
+
message: `Unknown component "${lesson.component}" and no override at components/${lesson.component}`,
|
|
591
|
+
severity: "error"
|
|
592
|
+
});
|
|
593
|
+
return issues;
|
|
594
|
+
}
|
|
595
|
+
const contained = assertResolvedPathContained(courseDir, resolved.path);
|
|
596
|
+
if (!contained.ok) {
|
|
597
|
+
issues.push({
|
|
598
|
+
path: `lessons.${lesson.id}.component`,
|
|
599
|
+
message: contained.message,
|
|
600
|
+
severity: "error"
|
|
601
|
+
});
|
|
602
|
+
return issues;
|
|
603
|
+
}
|
|
604
|
+
const componentStat = statSync4(resolved.path);
|
|
605
|
+
if (!componentStat.isFile()) {
|
|
606
|
+
issues.push({
|
|
607
|
+
path: `lessons.${lesson.id}.component`,
|
|
608
|
+
message: `Component override path is not a file: components/${lesson.component}`,
|
|
609
|
+
severity: "error"
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
return issues;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// src/validate/registry.ts
|
|
616
|
+
var lessonValidators = {
|
|
617
|
+
markdown: (courseDir, lesson) => validateMarkdownLesson(courseDir, lesson),
|
|
618
|
+
html: (courseDir, lesson) => validateHtmlLesson(courseDir, lesson),
|
|
619
|
+
component: (courseDir, lesson) => validateComponentLesson(courseDir, lesson)
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
// src/validate/ids.ts
|
|
623
|
+
function validateActivityIds(manifest) {
|
|
624
|
+
const issues = [];
|
|
625
|
+
const lessonIdCounts = /* @__PURE__ */ new Map();
|
|
626
|
+
for (const lesson of manifest.lessons) {
|
|
627
|
+
lessonIdCounts.set(lesson.id, (lessonIdCounts.get(lesson.id) ?? 0) + 1);
|
|
628
|
+
}
|
|
629
|
+
for (const [id, count] of lessonIdCounts) {
|
|
630
|
+
if (count > 1) {
|
|
631
|
+
issues.push({
|
|
632
|
+
path: "lessons",
|
|
633
|
+
message: `Duplicate lesson ID: ${id}`,
|
|
634
|
+
severity: "error"
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
const assessmentIdSet = /* @__PURE__ */ new Set();
|
|
639
|
+
for (const ref of manifest.assessments ?? []) {
|
|
640
|
+
assessmentIdSet.add(ref.id);
|
|
641
|
+
}
|
|
642
|
+
for (const lesson of manifest.lessons) {
|
|
643
|
+
if (assessmentIdSet.has(lesson.id)) {
|
|
644
|
+
issues.push({
|
|
645
|
+
path: "lessons",
|
|
646
|
+
message: `Lesson ID "${lesson.id}" conflicts with an assessment ID`,
|
|
647
|
+
severity: "error"
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return issues;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// src/validate.ts
|
|
655
|
+
function formatErrorMessage(err) {
|
|
656
|
+
return err instanceof Error ? err.message : String(err);
|
|
657
|
+
}
|
|
658
|
+
function formatIssuePath(path) {
|
|
659
|
+
const joined = path.map(String).join(".");
|
|
660
|
+
return joined || "course.yaml";
|
|
661
|
+
}
|
|
109
662
|
async function loadManifest(courseDir) {
|
|
110
|
-
const resolvedDir =
|
|
111
|
-
const manifestPath =
|
|
112
|
-
if (!
|
|
663
|
+
const resolvedDir = resolve2(courseDir);
|
|
664
|
+
const manifestPath = join3(resolvedDir, "course.yaml");
|
|
665
|
+
if (!existsSync5(manifestPath)) {
|
|
113
666
|
return [
|
|
114
667
|
{
|
|
115
668
|
path: "course.yaml",
|
|
@@ -120,8 +673,8 @@ async function loadManifest(courseDir) {
|
|
|
120
673
|
}
|
|
121
674
|
let raw;
|
|
122
675
|
try {
|
|
123
|
-
const content = await
|
|
124
|
-
raw =
|
|
676
|
+
const content = await readFile2(manifestPath, "utf-8");
|
|
677
|
+
raw = parseYaml2(content);
|
|
125
678
|
} catch (err) {
|
|
126
679
|
return [
|
|
127
680
|
{
|
|
@@ -143,198 +696,24 @@ async function loadManifest(courseDir) {
|
|
|
143
696
|
}
|
|
144
697
|
async function validateCourse(courseDir) {
|
|
145
698
|
const issues = [];
|
|
146
|
-
const resolvedDir =
|
|
699
|
+
const resolvedDir = resolve2(courseDir);
|
|
147
700
|
const loaded = await loadManifest(resolvedDir);
|
|
148
701
|
if (Array.isArray(loaded)) {
|
|
149
702
|
return { valid: false, issues: loaded };
|
|
150
703
|
}
|
|
151
704
|
const { manifest } = loaded;
|
|
152
705
|
for (const lesson of manifest.lessons) {
|
|
153
|
-
|
|
154
|
-
const resolved = resolveCoursePath(resolvedDir, lesson.file);
|
|
155
|
-
if (!resolved.ok) {
|
|
156
|
-
issues.push({
|
|
157
|
-
path: `lessons.${lesson.id}.file`,
|
|
158
|
-
message: resolved.message,
|
|
159
|
-
severity: "error"
|
|
160
|
-
});
|
|
161
|
-
continue;
|
|
162
|
-
}
|
|
163
|
-
if (!existsSync(resolved.path)) {
|
|
164
|
-
issues.push({
|
|
165
|
-
path: `lessons.${lesson.id}.file`,
|
|
166
|
-
message: `Lesson file not found: ${lesson.file}`,
|
|
167
|
-
severity: "error"
|
|
168
|
-
});
|
|
169
|
-
continue;
|
|
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
|
-
}
|
|
180
|
-
const stat = statSync(resolved.path);
|
|
181
|
-
if (!stat.isFile()) {
|
|
182
|
-
issues.push({
|
|
183
|
-
path: `lessons.${lesson.id}.file`,
|
|
184
|
-
message: `Lesson path is not a file: ${lesson.file}`,
|
|
185
|
-
severity: "error"
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
} else if (lesson.type === "html") {
|
|
189
|
-
const resolved = resolveCoursePath(resolvedDir, lesson.path);
|
|
190
|
-
if (!resolved.ok) {
|
|
191
|
-
issues.push({
|
|
192
|
-
path: `lessons.${lesson.id}.path`,
|
|
193
|
-
message: resolved.message,
|
|
194
|
-
severity: "error"
|
|
195
|
-
});
|
|
196
|
-
continue;
|
|
197
|
-
}
|
|
198
|
-
if (!existsSync(resolved.path)) {
|
|
199
|
-
issues.push({
|
|
200
|
-
path: `lessons.${lesson.id}.path`,
|
|
201
|
-
message: `HTML interaction directory not found: ${lesson.path}`,
|
|
202
|
-
severity: "error"
|
|
203
|
-
});
|
|
204
|
-
continue;
|
|
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
|
-
}
|
|
215
|
-
const stat = statSync(resolved.path);
|
|
216
|
-
if (!stat.isDirectory()) {
|
|
217
|
-
issues.push({
|
|
218
|
-
path: `lessons.${lesson.id}.path`,
|
|
219
|
-
message: `HTML interaction path is not a directory: ${lesson.path}`,
|
|
220
|
-
severity: "error"
|
|
221
|
-
});
|
|
222
|
-
continue;
|
|
223
|
-
}
|
|
224
|
-
const indexPath = join(resolved.path, "index.html");
|
|
225
|
-
if (!existsSync(indexPath)) {
|
|
226
|
-
issues.push({
|
|
227
|
-
path: `lessons.${lesson.id}.path`,
|
|
228
|
-
message: `HTML interaction missing index.html: ${lesson.path}`,
|
|
229
|
-
severity: "error"
|
|
230
|
-
});
|
|
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
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
if (manifest.assessments) {
|
|
253
|
-
const assessmentIds = /* @__PURE__ */ new Set();
|
|
254
|
-
for (const ref of manifest.assessments) {
|
|
255
|
-
if (assessmentIds.has(ref.id)) {
|
|
256
|
-
issues.push({
|
|
257
|
-
path: "assessments",
|
|
258
|
-
message: `Duplicate assessment ID: ${ref.id}`,
|
|
259
|
-
severity: "error"
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
assessmentIds.add(ref.id);
|
|
263
|
-
const resolved = resolveCoursePath(resolvedDir, ref.file);
|
|
264
|
-
if (!resolved.ok) {
|
|
265
|
-
issues.push({
|
|
266
|
-
path: `assessments.${ref.id}.file`,
|
|
267
|
-
message: resolved.message,
|
|
268
|
-
severity: "error"
|
|
269
|
-
});
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
if (!existsSync(resolved.path)) {
|
|
273
|
-
issues.push({
|
|
274
|
-
path: `assessments.${ref.id}.file`,
|
|
275
|
-
message: `Assessment file not found: ${ref.file}`,
|
|
276
|
-
severity: "error"
|
|
277
|
-
});
|
|
278
|
-
continue;
|
|
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
|
-
}
|
|
298
|
-
try {
|
|
299
|
-
const content = await readFile(resolved.path, "utf-8");
|
|
300
|
-
const raw = parseYaml(content);
|
|
301
|
-
const parsed = assessmentSchema.safeParse(raw);
|
|
302
|
-
if (!parsed.success) {
|
|
303
|
-
for (const issue of parsed.error.issues) {
|
|
304
|
-
const subPath = issue.path.length ? issue.path.join(".") : "root";
|
|
305
|
-
issues.push({
|
|
306
|
-
path: `${ref.file}:${subPath}`,
|
|
307
|
-
message: issue.message,
|
|
308
|
-
severity: "error"
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
if (parsed.data.id !== ref.id) {
|
|
314
|
-
issues.push({
|
|
315
|
-
path: `assessments.${ref.id}`,
|
|
316
|
-
message: `Assessment file id "${parsed.data.id}" does not match manifest ref id "${ref.id}"`,
|
|
317
|
-
severity: "error"
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
} catch (err) {
|
|
321
|
-
issues.push({
|
|
322
|
-
path: ref.file,
|
|
323
|
-
message: `Failed to parse assessment: ${formatErrorMessage(err)}`,
|
|
324
|
-
severity: "error"
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
const lessonIdCounts = /* @__PURE__ */ new Map();
|
|
330
|
-
for (const lesson of manifest.lessons) {
|
|
331
|
-
lessonIdCounts.set(lesson.id, (lessonIdCounts.get(lesson.id) ?? 0) + 1);
|
|
706
|
+
issues.push(...lessonValidators[lesson.type](resolvedDir, lesson));
|
|
332
707
|
}
|
|
333
|
-
|
|
334
|
-
|
|
708
|
+
const assessmentLoad = await loadParsedAssessments(resolvedDir, manifest);
|
|
709
|
+
issues.push(...assessmentLoad.issues);
|
|
710
|
+
issues.push(...validateActivityIds(manifest));
|
|
711
|
+
issues.push(...validateFlow(manifest));
|
|
712
|
+
if (manifest.flow?.length) {
|
|
713
|
+
for (const message of detectFlowCycles(manifest.flow)) {
|
|
335
714
|
issues.push({
|
|
336
|
-
path: "
|
|
337
|
-
message
|
|
715
|
+
path: "flow",
|
|
716
|
+
message,
|
|
338
717
|
severity: "error"
|
|
339
718
|
});
|
|
340
719
|
}
|
|
@@ -342,80 +721,64 @@ async function validateCourse(courseDir) {
|
|
|
342
721
|
return {
|
|
343
722
|
valid: issues.filter((i) => i.severity === "error").length === 0,
|
|
344
723
|
manifest,
|
|
345
|
-
issues
|
|
724
|
+
issues,
|
|
725
|
+
parsedAssessments: assessmentLoad.parsed
|
|
346
726
|
};
|
|
347
727
|
}
|
|
348
728
|
|
|
349
|
-
// src/
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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 = {};
|
|
729
|
+
// src/activities.ts
|
|
730
|
+
function enumerateActivities(manifest) {
|
|
731
|
+
const activities = manifest.lessons.map((lesson) => ({
|
|
732
|
+
id: lesson.id,
|
|
733
|
+
title: lesson.title ?? lesson.id,
|
|
734
|
+
kind: "lesson"
|
|
735
|
+
}));
|
|
378
736
|
for (const ref of manifest.assessments ?? []) {
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
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;
|
|
737
|
+
activities.push({
|
|
738
|
+
id: ref.id,
|
|
739
|
+
title: ref.id.replace(/_/g, " "),
|
|
740
|
+
kind: "assessment"
|
|
741
|
+
});
|
|
399
742
|
}
|
|
400
|
-
return
|
|
743
|
+
return activities;
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// src/html.ts
|
|
747
|
+
function escapeHtml(text) {
|
|
748
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
401
749
|
}
|
|
402
750
|
export {
|
|
751
|
+
BUILTIN_COMPONENT_IDS,
|
|
403
752
|
assertResolvedPathContained,
|
|
404
753
|
assessmentQuestionSchema,
|
|
405
754
|
assessmentRefSchema,
|
|
406
755
|
assessmentSchema,
|
|
407
756
|
buildRuntimeAssessmentBundle,
|
|
757
|
+
buildRuntimeAssessmentBundleFromParsed,
|
|
758
|
+
collectActivityIds,
|
|
759
|
+
collectAssessmentIds,
|
|
760
|
+
componentLessonSchema,
|
|
761
|
+
conditionSchema,
|
|
408
762
|
courseManifestSchema,
|
|
763
|
+
detectFlowCycles,
|
|
764
|
+
enumerateActivities,
|
|
765
|
+
escapeHtml,
|
|
766
|
+
flowRuleSchema,
|
|
409
767
|
formatErrorMessage,
|
|
410
768
|
formatIssuePath,
|
|
411
769
|
htmlLessonSchema,
|
|
770
|
+
isBuiltinComponentId,
|
|
412
771
|
isPathContained,
|
|
413
772
|
lessonSchema,
|
|
414
773
|
loadManifest,
|
|
774
|
+
loadParsedAssessments,
|
|
415
775
|
markdownLessonSchema,
|
|
416
776
|
resolveCoursePath,
|
|
417
777
|
runtimeConfigSchema,
|
|
778
|
+
showFeedbackSchema,
|
|
418
779
|
toLearnerAssessment,
|
|
419
780
|
trackingSchema,
|
|
420
|
-
validateCourse
|
|
781
|
+
validateCourse,
|
|
782
|
+
validateFlow,
|
|
783
|
+
variableDefSchema
|
|
421
784
|
};
|