@lxpack/validators 0.2.1 → 0.2.2
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 +2 -2
- package/dist/index.d.ts +44 -4
- package/dist/index.js +146 -30
- 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 — including flow, variables, and component lessons (v0.2.
|
|
8
|
+
Zod schemas and filesystem validation for LXPack course manifests — including flow, variables, and component lessons (v0.2.2).
|
|
9
9
|
|
|
10
10
|
Part of [LXPack](https://github.com/eddiethedean/lxpack) — an AI-native learning experience compiler and runtime.
|
|
11
11
|
|
|
@@ -94,7 +94,7 @@ isPathContained(courseDir, abs); // true if inside course root
|
|
|
94
94
|
| `buildRuntimeAssessmentBundle(dir, manifest)` | Load assessments; split learner view, keys, configs, feedback |
|
|
95
95
|
| `toLearnerAssessment(assessment)` | Strip `correct` from choices; extract config and feedback maps |
|
|
96
96
|
| `validateFlow(manifest)` | Flow rule and target validation |
|
|
97
|
-
| `detectFlowCycles(
|
|
97
|
+
| `detectFlowCycles(manifest)` | Flow-jump cycle detection for branching graphs |
|
|
98
98
|
| `collectActivityIds(manifest)` | Lesson and assessment IDs for flow targets |
|
|
99
99
|
| `conditionSchema`, `flowRuleSchema` | Zod schemas for flow conditions and rules |
|
|
100
100
|
| `BUILTIN_COMPONENT_IDS`, `isBuiltinComponentId` | Allowed built-in component lesson IDs |
|
package/dist/index.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ declare const flowRuleSchema: z.ZodObject<{
|
|
|
30
30
|
}>;
|
|
31
31
|
type FlowRule = z.infer<typeof flowRuleSchema>;
|
|
32
32
|
|
|
33
|
+
/** Safe for SCORM paths and manifest identifiers. */
|
|
34
|
+
declare const activityIdSchema: z.ZodString;
|
|
33
35
|
declare const assessmentQuestionSchema: z.ZodEffects<z.ZodObject<{
|
|
34
36
|
id: z.ZodString;
|
|
35
37
|
prompt: z.ZodString;
|
|
@@ -185,7 +187,7 @@ declare const lessonSchema: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
|
185
187
|
props?: Record<string, unknown> | undefined;
|
|
186
188
|
}>]>;
|
|
187
189
|
declare const showFeedbackSchema: z.ZodDefault<z.ZodEnum<["immediate", "end", "never"]>>;
|
|
188
|
-
declare const assessmentSchema: z.ZodObject<{
|
|
190
|
+
declare const assessmentSchema: z.ZodEffects<z.ZodObject<{
|
|
189
191
|
id: z.ZodString;
|
|
190
192
|
title: z.ZodOptional<z.ZodString>;
|
|
191
193
|
passingScore: z.ZodDefault<z.ZodNumber>;
|
|
@@ -280,6 +282,40 @@ declare const assessmentSchema: z.ZodObject<{
|
|
|
280
282
|
maxAttempts?: number | undefined;
|
|
281
283
|
shuffleChoices?: boolean | undefined;
|
|
282
284
|
showFeedback?: "never" | "immediate" | "end" | undefined;
|
|
285
|
+
}>, {
|
|
286
|
+
id: string;
|
|
287
|
+
passingScore: number;
|
|
288
|
+
questions: {
|
|
289
|
+
id: string;
|
|
290
|
+
prompt: string;
|
|
291
|
+
choices: {
|
|
292
|
+
id: string;
|
|
293
|
+
text: string;
|
|
294
|
+
correct?: boolean | undefined;
|
|
295
|
+
}[];
|
|
296
|
+
explanation?: string | undefined;
|
|
297
|
+
}[];
|
|
298
|
+
title?: string | undefined;
|
|
299
|
+
maxAttempts?: number | undefined;
|
|
300
|
+
shuffleChoices?: boolean | undefined;
|
|
301
|
+
showFeedback?: "never" | "immediate" | "end" | undefined;
|
|
302
|
+
}, {
|
|
303
|
+
id: string;
|
|
304
|
+
questions: {
|
|
305
|
+
id: string;
|
|
306
|
+
prompt: string;
|
|
307
|
+
choices: {
|
|
308
|
+
id: string;
|
|
309
|
+
text: string;
|
|
310
|
+
correct?: boolean | undefined;
|
|
311
|
+
}[];
|
|
312
|
+
explanation?: string | undefined;
|
|
313
|
+
}[];
|
|
314
|
+
title?: string | undefined;
|
|
315
|
+
passingScore?: number | undefined;
|
|
316
|
+
maxAttempts?: number | undefined;
|
|
317
|
+
shuffleChoices?: boolean | undefined;
|
|
318
|
+
showFeedback?: "never" | "immediate" | "end" | undefined;
|
|
283
319
|
}>;
|
|
284
320
|
declare const assessmentRefSchema: z.ZodObject<{
|
|
285
321
|
id: z.ZodString;
|
|
@@ -578,9 +614,13 @@ declare function validateCourse(courseDir: string): Promise<ValidationResult>;
|
|
|
578
614
|
|
|
579
615
|
declare function collectActivityIds(manifest: CourseManifest): Set<string>;
|
|
580
616
|
declare function collectAssessmentIds(manifest: CourseManifest): Set<string>;
|
|
617
|
+
declare function collectInteractionIds(manifest: CourseManifest): Set<string>;
|
|
618
|
+
declare function buildActivityOrder(manifest: CourseManifest): string[];
|
|
581
619
|
declare function validateFlow(manifest: CourseManifest): ValidationIssue[];
|
|
582
|
-
/**
|
|
583
|
-
|
|
620
|
+
/**
|
|
621
|
+
* Detect cycles reachable via flow jumps (first matching applicable rule).
|
|
622
|
+
*/
|
|
623
|
+
declare function detectFlowCycles(manifest: CourseManifest): string[];
|
|
584
624
|
|
|
585
625
|
interface LearnerChoice {
|
|
586
626
|
id: string;
|
|
@@ -635,4 +675,4 @@ declare function enumerateActivities(manifest: CourseManifest): CourseActivity[]
|
|
|
635
675
|
|
|
636
676
|
declare function escapeHtml(text: string): string;
|
|
637
677
|
|
|
638
|
-
export { type Assessment, type AssessmentRef, type AssessmentRuntimeConfig, BUILTIN_COMPONENT_IDS, type BuiltinComponentId, type ComponentLesson, type Condition, type CourseActivity, 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, buildRuntimeAssessmentBundleFromParsed, collectActivityIds, collectAssessmentIds, componentLessonSchema, conditionSchema, courseManifestSchema, detectFlowCycles, enumerateActivities, escapeHtml, flowRuleSchema, formatErrorMessage, formatIssuePath, htmlLessonSchema, isBuiltinComponentId, isPathContained, lessonSchema, loadManifest, loadParsedAssessments, markdownLessonSchema, resolveCoursePath, runtimeConfigSchema, showFeedbackSchema, toLearnerAssessment, trackingSchema, validateCourse, validateFlow, variableDefSchema };
|
|
678
|
+
export { type Assessment, type AssessmentRef, type AssessmentRuntimeConfig, BUILTIN_COMPONENT_IDS, type BuiltinComponentId, type ComponentLesson, type Condition, type CourseActivity, type CourseManifest, type FlowRule, type LearnerAssessment, type LearnerChoice, type LearnerQuestion, type Lesson, type QuestionFeedback, type RuntimeAssessmentBundle, type ShowFeedback, type ValidationIssue, type ValidationResult, type VariableDef, activityIdSchema, assertResolvedPathContained, assessmentQuestionSchema, assessmentRefSchema, assessmentSchema, buildActivityOrder, buildRuntimeAssessmentBundle, buildRuntimeAssessmentBundleFromParsed, collectActivityIds, collectAssessmentIds, collectInteractionIds, componentLessonSchema, conditionSchema, courseManifestSchema, detectFlowCycles, enumerateActivities, escapeHtml, flowRuleSchema, formatErrorMessage, formatIssuePath, htmlLessonSchema, isBuiltinComponentId, isPathContained, lessonSchema, loadManifest, loadParsedAssessments, markdownLessonSchema, resolveCoursePath, runtimeConfigSchema, showFeedbackSchema, toLearnerAssessment, trackingSchema, validateCourse, validateFlow, variableDefSchema };
|
package/dist/index.js
CHANGED
|
@@ -36,6 +36,10 @@ var flowRuleSchema = z.object({
|
|
|
36
36
|
}).strict();
|
|
37
37
|
|
|
38
38
|
// src/schemas.ts
|
|
39
|
+
var activityIdSchema = z2.string().min(1).regex(
|
|
40
|
+
/^[a-zA-Z][a-zA-Z0-9_-]*$/,
|
|
41
|
+
"ID must start with a letter and contain only letters, numbers, underscores, and hyphens"
|
|
42
|
+
);
|
|
39
43
|
var choiceSchema = z2.object({
|
|
40
44
|
id: z2.string().min(1),
|
|
41
45
|
text: z2.string().min(1),
|
|
@@ -55,21 +59,33 @@ var assessmentQuestionSchema = z2.object({
|
|
|
55
59
|
path: ["choices"]
|
|
56
60
|
});
|
|
57
61
|
}
|
|
62
|
+
const choiceIds = /* @__PURE__ */ new Set();
|
|
63
|
+
for (let i = 0; i < question.choices.length; i++) {
|
|
64
|
+
const choice = question.choices[i];
|
|
65
|
+
if (choiceIds.has(choice.id)) {
|
|
66
|
+
ctx.addIssue({
|
|
67
|
+
code: z2.ZodIssueCode.custom,
|
|
68
|
+
message: `Duplicate choice id: ${choice.id}`,
|
|
69
|
+
path: ["choices", i, "id"]
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
choiceIds.add(choice.id);
|
|
73
|
+
}
|
|
58
74
|
});
|
|
59
75
|
var markdownLessonSchema = z2.object({
|
|
60
|
-
id:
|
|
76
|
+
id: activityIdSchema,
|
|
61
77
|
type: z2.literal("markdown"),
|
|
62
78
|
file: z2.string().min(1),
|
|
63
79
|
title: z2.string().optional()
|
|
64
80
|
}).strict();
|
|
65
81
|
var htmlLessonSchema = z2.object({
|
|
66
|
-
id:
|
|
82
|
+
id: activityIdSchema,
|
|
67
83
|
type: z2.literal("html"),
|
|
68
84
|
path: z2.string().min(1),
|
|
69
85
|
title: z2.string().optional()
|
|
70
86
|
}).strict();
|
|
71
87
|
var componentLessonSchema = z2.object({
|
|
72
|
-
id:
|
|
88
|
+
id: activityIdSchema,
|
|
73
89
|
type: z2.literal("component"),
|
|
74
90
|
component: z2.string().min(1),
|
|
75
91
|
props: z2.record(z2.unknown()).optional(),
|
|
@@ -82,16 +98,29 @@ var lessonSchema = z2.discriminatedUnion("type", [
|
|
|
82
98
|
]);
|
|
83
99
|
var showFeedbackSchema = z2.enum(["immediate", "end", "never"]).default("never");
|
|
84
100
|
var assessmentSchema = z2.object({
|
|
85
|
-
id:
|
|
101
|
+
id: activityIdSchema,
|
|
86
102
|
title: z2.string().optional(),
|
|
87
103
|
passingScore: z2.number().min(0).max(1).default(0.7),
|
|
88
104
|
maxAttempts: z2.number().int().min(1).optional(),
|
|
89
105
|
shuffleChoices: z2.boolean().optional(),
|
|
90
106
|
showFeedback: showFeedbackSchema.optional(),
|
|
91
107
|
questions: z2.array(assessmentQuestionSchema).min(1)
|
|
92
|
-
}).strict()
|
|
108
|
+
}).strict().superRefine((assessment, ctx) => {
|
|
109
|
+
const questionIds = /* @__PURE__ */ new Set();
|
|
110
|
+
for (let i = 0; i < assessment.questions.length; i++) {
|
|
111
|
+
const q = assessment.questions[i];
|
|
112
|
+
if (questionIds.has(q.id)) {
|
|
113
|
+
ctx.addIssue({
|
|
114
|
+
code: z2.ZodIssueCode.custom,
|
|
115
|
+
message: `Duplicate question id: ${q.id}`,
|
|
116
|
+
path: ["questions", i, "id"]
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
questionIds.add(q.id);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
93
122
|
var assessmentRefSchema = z2.object({
|
|
94
|
-
id:
|
|
123
|
+
id: activityIdSchema,
|
|
95
124
|
file: z2.string().min(1)
|
|
96
125
|
}).strict();
|
|
97
126
|
var variableDefSchema = z2.object({
|
|
@@ -169,6 +198,54 @@ function collectAssessmentIds(manifest) {
|
|
|
169
198
|
}
|
|
170
199
|
return ids;
|
|
171
200
|
}
|
|
201
|
+
function collectInteractionIds(manifest) {
|
|
202
|
+
const ids = /* @__PURE__ */ new Set();
|
|
203
|
+
for (const lesson of manifest.lessons) {
|
|
204
|
+
if (lesson.type === "html") {
|
|
205
|
+
ids.add(lesson.id);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return ids;
|
|
209
|
+
}
|
|
210
|
+
function buildActivityOrder(manifest) {
|
|
211
|
+
const ids = manifest.lessons.map((l) => l.id);
|
|
212
|
+
for (const ref of manifest.assessments ?? []) {
|
|
213
|
+
ids.push(ref.id);
|
|
214
|
+
}
|
|
215
|
+
return ids;
|
|
216
|
+
}
|
|
217
|
+
function validateConditionShape(condition, path) {
|
|
218
|
+
const issues = [];
|
|
219
|
+
if ("all" in condition && condition.all) {
|
|
220
|
+
if (condition.all.length === 0) {
|
|
221
|
+
issues.push({
|
|
222
|
+
path,
|
|
223
|
+
message: "Condition all: [] is always true at runtime; use a non-empty list",
|
|
224
|
+
severity: "error"
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
for (let i = 0; i < condition.all.length; i++) {
|
|
228
|
+
issues.push(
|
|
229
|
+
...validateConditionShape(condition.all[i], `${path}.all[${i}]`)
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if ("any" in condition && condition.any) {
|
|
234
|
+
if (condition.any.length === 0) {
|
|
235
|
+
issues.push({
|
|
236
|
+
path,
|
|
237
|
+
message: "Condition any: [] is always false at runtime; use a non-empty list",
|
|
238
|
+
severity: "error"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
for (let i = 0; i < condition.any.length; i++) {
|
|
242
|
+
issues.push(
|
|
243
|
+
...validateConditionShape(condition.any[i], `${path}.any[${i}]`)
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return issues;
|
|
248
|
+
}
|
|
172
249
|
function collectConditionRefs(condition, refs) {
|
|
173
250
|
if ("variable" in condition && condition.variable?.eq) {
|
|
174
251
|
refs.variables.add(condition.variable.eq[0]);
|
|
@@ -192,9 +269,11 @@ function validateFlow(manifest) {
|
|
|
192
269
|
if (!flow?.length) return issues;
|
|
193
270
|
const activityIds = collectActivityIds(manifest);
|
|
194
271
|
const assessmentIds = collectAssessmentIds(manifest);
|
|
272
|
+
const interactionIds = collectInteractionIds(manifest);
|
|
195
273
|
const manifestVars = new Set(Object.keys(manifest.variables ?? {}));
|
|
196
274
|
flow.forEach((rule, index) => {
|
|
197
275
|
const path = `flow[${index}]`;
|
|
276
|
+
issues.push(...validateConditionShape(rule.when, `${path}.when`));
|
|
198
277
|
if (!activityIds.has(rule.goto)) {
|
|
199
278
|
issues.push({
|
|
200
279
|
path: `${path}.goto`,
|
|
@@ -227,10 +306,10 @@ function validateFlow(manifest) {
|
|
|
227
306
|
}
|
|
228
307
|
}
|
|
229
308
|
for (const i of refs.interactions) {
|
|
230
|
-
if (!
|
|
309
|
+
if (!interactionIds.has(i)) {
|
|
231
310
|
issues.push({
|
|
232
311
|
path: `${path}.when`,
|
|
233
|
-
message: `Unknown interaction
|
|
312
|
+
message: `Unknown interaction id in condition (expected html lesson): ${i}`,
|
|
234
313
|
severity: "error"
|
|
235
314
|
});
|
|
236
315
|
}
|
|
@@ -238,30 +317,63 @@ function validateFlow(manifest) {
|
|
|
238
317
|
});
|
|
239
318
|
return issues;
|
|
240
319
|
}
|
|
241
|
-
function
|
|
242
|
-
|
|
243
|
-
|
|
320
|
+
function conditionCouldApplyAt(condition, currentActivityId, interactionIds) {
|
|
321
|
+
if ("variable" in condition) return true;
|
|
322
|
+
if ("assessment" in condition && condition.assessment?.passed) {
|
|
323
|
+
return currentActivityId === condition.assessment.passed;
|
|
324
|
+
}
|
|
325
|
+
if ("interaction" in condition && condition.interaction?.done) {
|
|
326
|
+
return interactionIds.has(currentActivityId);
|
|
327
|
+
}
|
|
328
|
+
if ("all" in condition && condition.all) {
|
|
329
|
+
return condition.all.length > 0 && condition.all.every(
|
|
330
|
+
(c) => conditionCouldApplyAt(c, currentActivityId, interactionIds)
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
if ("any" in condition && condition.any) {
|
|
334
|
+
return condition.any.length > 0 && condition.any.some(
|
|
335
|
+
(c) => conditionCouldApplyAt(c, currentActivityId, interactionIds)
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
function detectFlowCycles(manifest) {
|
|
341
|
+
const flow = manifest.flow;
|
|
342
|
+
if (!flow?.length) return [];
|
|
343
|
+
const activityIds = collectActivityIds(manifest);
|
|
344
|
+
const interactionIds = collectInteractionIds(manifest);
|
|
345
|
+
const flowJumpFrom = (current) => {
|
|
346
|
+
for (const rule of flow) {
|
|
347
|
+
if (rule.goto !== current && activityIds.has(rule.goto) && conditionCouldApplyAt(rule.when, current, interactionIds)) {
|
|
348
|
+
return rule.goto;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return null;
|
|
352
|
+
};
|
|
244
353
|
const errors = [];
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
354
|
+
const reported = /* @__PURE__ */ new Set();
|
|
355
|
+
const dfs = (node, stack, onStack) => {
|
|
356
|
+
const jump = flowJumpFrom(node);
|
|
357
|
+
if (!jump) return;
|
|
358
|
+
if (onStack.has(jump)) {
|
|
359
|
+
const cycleStart = stack.indexOf(jump);
|
|
360
|
+
const cycle = [...stack.slice(cycleStart), jump];
|
|
361
|
+
const key = cycle.join("->");
|
|
362
|
+
if (!reported.has(key)) {
|
|
363
|
+
reported.add(key);
|
|
364
|
+
errors.push(`Flow cycle: ${cycle.join(" -> ")}`);
|
|
255
365
|
}
|
|
256
|
-
|
|
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;
|
|
366
|
+
return;
|
|
264
367
|
}
|
|
368
|
+
if (stack.length >= activityIds.size) return;
|
|
369
|
+
onStack.add(jump);
|
|
370
|
+
stack.push(jump);
|
|
371
|
+
dfs(jump, stack, onStack);
|
|
372
|
+
stack.pop();
|
|
373
|
+
onStack.delete(jump);
|
|
374
|
+
};
|
|
375
|
+
for (const id of activityIds) {
|
|
376
|
+
dfs(id, [id], /* @__PURE__ */ new Set([id]));
|
|
265
377
|
}
|
|
266
378
|
return errors;
|
|
267
379
|
}
|
|
@@ -359,6 +471,7 @@ async function loadParsedAssessments(courseDir, manifest) {
|
|
|
359
471
|
message: `Duplicate assessment ID: ${ref.id}`,
|
|
360
472
|
severity: "error"
|
|
361
473
|
});
|
|
474
|
+
continue;
|
|
362
475
|
}
|
|
363
476
|
assessmentIds.add(ref.id);
|
|
364
477
|
const resolved = resolveCoursePath(resolvedDir, ref.file);
|
|
@@ -710,7 +823,7 @@ async function validateCourse(courseDir) {
|
|
|
710
823
|
issues.push(...validateActivityIds(manifest));
|
|
711
824
|
issues.push(...validateFlow(manifest));
|
|
712
825
|
if (manifest.flow?.length) {
|
|
713
|
-
for (const message of detectFlowCycles(manifest
|
|
826
|
+
for (const message of detectFlowCycles(manifest)) {
|
|
714
827
|
issues.push({
|
|
715
828
|
path: "flow",
|
|
716
829
|
message,
|
|
@@ -749,14 +862,17 @@ function escapeHtml(text) {
|
|
|
749
862
|
}
|
|
750
863
|
export {
|
|
751
864
|
BUILTIN_COMPONENT_IDS,
|
|
865
|
+
activityIdSchema,
|
|
752
866
|
assertResolvedPathContained,
|
|
753
867
|
assessmentQuestionSchema,
|
|
754
868
|
assessmentRefSchema,
|
|
755
869
|
assessmentSchema,
|
|
870
|
+
buildActivityOrder,
|
|
756
871
|
buildRuntimeAssessmentBundle,
|
|
757
872
|
buildRuntimeAssessmentBundleFromParsed,
|
|
758
873
|
collectActivityIds,
|
|
759
874
|
collectAssessmentIds,
|
|
875
|
+
collectInteractionIds,
|
|
760
876
|
componentLessonSchema,
|
|
761
877
|
conditionSchema,
|
|
762
878
|
courseManifestSchema,
|