@lxpack/validators
v0.7.0
Published
Course manifest validation for LXPack
Maintainers
Readme
@lxpack/validators
Zod schemas and filesystem validation for LXPack course manifests — flow, variables, component lessons, and xAPI tracking.
Part of LXPack — an AI-native learning experience compiler and runtime. Docs: course.yaml · Troubleshooting.
| Related | Package |
|---------|---------|
| CLI | @lxpack/cli |
| Programmatic API | @lxpack/api |
| Packaging | @lxpack/scorm |
| Runtime | @lxpack/runtime |
| Components | @lxpack/components |
Install
npm install @lxpack/validatorsRequires Node.js 18 or 20 (18+).
Usage
import {
validateCourse,
validateCourseManifest,
loadManifest,
buildRuntimeAssessmentBundle,
buildRuntimeAssessmentBundleFromData,
courseManifestSchema,
type ValidationResult,
} from "@lxpack/validators";
const result: ValidationResult = await validateCourse("/path/to/my-course");
if (!result.valid) {
for (const issue of result.issues) {
console.error(`${issue.path}: ${issue.message}`);
}
} else {
const { manifest } = result;
const bundle = await buildRuntimeAssessmentBundle("/path/to/my-course", manifest);
// bundle.assessments — learner-facing questions (no correct flags)
// bundle.answerKeys — scoring keys for the runtime
// bundle.configs — maxAttempts, shuffleChoices, showFeedback per assessment
// bundle.feedback — questionId → explanation text for feedback modes
}Load and parse only
const loaded = await loadManifest("/path/to/my-course");
if (Array.isArray(loaded)) {
// validation issues
} else {
const { manifest } = loaded;
}Schema-only validation
const parsed = courseManifestSchema.safeParse(manifestObject);Flow validation
import { validateFlow, detectFlowCycles, collectActivityIds } from "@lxpack/validators";validateCourse runs flow checks when manifest.flow is present: valid goto targets, known condition shapes, and cycle detection.
Safe path resolution
import { resolveCoursePath, isPathContained } from "@lxpack/validators";
const abs = resolveCoursePath(courseDir, "lessons/intro.md");
isPathContained(courseDir, abs); // true if inside course rootExports
| Export | Description |
|--------|-------------|
| validateCourse(dir) | Parse course.yaml, validate schema, flow, files, symlink containment |
| validateCourseManifest(dir, manifest, options?) | Validate an in-memory manifest (optional assessmentData instead of on-disk YAML) |
| validateXapiTracking(manifest) | Require HTTPS tracking.xapi.activityIri for xapi/cmi5 exports |
| getCourseActivityIri(manifest) | Read course activity IRI from manifest |
| loadManifest(courseDir) | Load and parse course.yaml |
| buildRuntimeAssessmentBundle(dir, manifest) | Load assessments; split learner view, keys, configs, feedback |
| buildRuntimeAssessmentBundleFromData(manifest, data) | Same bundle shape from in-memory assessment objects |
| loadParsedAssessmentsFromData(manifest, data) | Parse injected assessment payloads with manifest cross-checks |
| toLearnerAssessment(assessment) | Strip correct from choices; build string \| string[] answer keys; extract config and feedback maps |
| AnswerKeyValue, SelectionMode | Types for per-question answer keys and selection mode |
| validateFlow(manifest) | Flow rule and target validation |
| detectFlowCycles(manifest) | Flow-jump cycle detection for branching graphs |
| collectActivityIds(manifest) | Lesson and assessment IDs for flow targets |
| conditionSchema, flowRuleSchema | Zod schemas for flow conditions and rules |
| BUILTIN_COMPONENT_IDS, isBuiltinComponentId | Allowed built-in component lesson IDs |
| resolveCoursePath(dir, relativePath) | Resolve a path safely inside the course directory |
| isPathContained(root, target) | Whether target stays under root |
| courseManifestSchema | Zod schema for the full course manifest |
| lessonSchema, assessmentSchema, variableDefSchema, … | Strict sub-schemas |
| CourseManifest, Lesson, Assessment, FlowRule, VariableDef, RuntimeAssessmentBundle | TypeScript types |
What gets validated
Author-facing rules: Course structure · Quizzes and assessments · Branching and paths.
- Manifest shape: lessons, assessments, optional
variablesandflow, tracking rules - Lesson types:
markdown(file),html(path),spa(pathwithindex.html),component(component+ optionalprops) - SPA lessons: path containment, required
index.html; warns when SPA HTML callswindow.lxpackinstead ofwindow.parent.lxpackBridge.v1 runtime.cssVariables— optional map of CSS custom properties for the learner shell- Component IDs: built-in IDs or course overrides under
components/<id>/ - Flow rules: condition grammar,
gototargets that reference known activity IDs, acyclic flow (errors for cycles) - Assessment YAML: strict MCQ schemas (single- or multi-select); optional
selectionMode,maxAttempts,shuffleChoices,showFeedback;explanationper question - Duplicate lesson IDs
- Path containment — referenced files must stay inside the course directory (including via symlinks)
- On-disk assets: files exist and assessment paths are regular files
Assessment packaging
Author assessments live as YAML under assessments/ in the course repo. At build/preview time:
buildRuntimeAssessmentBundle()reads each assessment file.- Learner payload — questions and choices without
correctflags. - Answer keys —
questionId → choiceId(single-select) orquestionId → choiceId[](multi-select) for scoring. Additive shape:AnswerKeyValue = string | string[]— existing single-select courses keep string keys. - Configs — per-assessment quiz behavior (
maxAttempts,shuffleChoices,showFeedback). - Feedback —
questionId → explanationfor immediate/end feedback (not shipped as separate files).
The CLI and @lxpack/scorm 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.
Development
From the monorepo root:
pnpm --filter @lxpack/validators build
pnpm --filter @lxpack/validators test
pnpm --filter @lxpack/validators typecheckLinks
License
Apache-2.0
