LXPack interoperability

Upgrade plan for LXPack maintainers

Forward-looking priorities, responsibility shifts, and API proposals for the LXPack project:

LessonKit authors React courses; LXPack validates, packages, and runs them in LMS contexts. v0.4.0 delivered the critical baseline (SPA lessons, @lxpack/api, lessonkit.json merge, in-memory assessments, lxpackBridge.v1). v0.6.0 adds packageLessonkit(), interchange schema in validators, @lxpack/spa-bridge, and interchange runtime (theme/cssVariables).

Remaining wins are conformance and optional meta-packages, not core packaging. LessonKit should keep React authoring, telemetry catalog, and thin CLI wiring.

Target end state: @lessonkit/lxpack becomes a small facade (descriptor → interchange + path helpers), or authors depend on @lxpack/api + @lxpack/lessonkit directly.


Current integration (baseline)

What LessonKit does today (@lessonkit/lxpack)

Step

Owner today

Notes

Validate LessonkitCourseDescriptor

LessonKit

Uses @lessonkit/core id rules + layout rules

Copy Vite dist/ into LXPack tree

LessonKit

single-spa{outDir}/dist; per-lesson-spa → per-lesson paths

Materialize LXPack project from interchange

LXPack (packageLessonkit / materializeLessonkitProject)

LessonKit supplies interchange + spaDirs

Emit authoring YAML (optional)

LXPack when writeAuthoringFiles: true

Legacy writeLxpackProject() still available for tests

Build interchange JSON

LessonKit

descriptorToInterchange()@lxpack/validators schema

Map theme → runtime.cssVariables

LessonKit descriptor → interchange runtime

LXPack resolves presets at materialize

validateCourse / buildCourse

LXPack (@lxpack/api)

Via packageLessonkit()

Staging + atomic promote to outDir

LessonKit

Temp dir; rollback on failure

Runtime bridge (browser)

@lxpack/spa-bridge

Re-exported from @lessonkit/lxpack/bridge; React forwards telemetry

What LXPack already owns (keep)

  • SCORM 1.2 / 2004 / standalone / xAPI / cmi5 packaging

  • course.yaml schema + path containment

  • Learner shell, flow, native quiz engine, LMS APIs

  • SPA iframe + parent bridge host

  • @lxpack/tracking-schema (native LXPack courses)

Pain points driving this plan

  1. Duplicate manifest authoring — LessonKit re-implements YAML generation and assessment shaping that LXPack validators already understand.

  2. Two sources of truth for assessments — On-disk YAML and buildCourse({ assessments }); easy to drift.

  3. Bridge contract is implicit — Score scale (0–1 vs points), method names, and versioning are documented in LessonKit, not as a typed LXPack package.

  4. No first-class “package from interchange” API — Adapter must write a full LXPack project tree before every build.

  5. Tracking vocabularies diverge — LessonKit telemetry events vs LXPack track() / xAPI export paths are aligned by convention, not schema.

  6. Preview gap — Authors use Vite for dev; LXPack preview requires a materialized project; parity testing is manual.

  7. Conformance is one-sided — LessonKit 0.9.x plans Playwright parity tests; LXPack has no shared fixture contract.



Proposed LXPack upgrades (prioritized)

P0 — Package LessonKit without a hand-built project tree

Problem: @lessonkit/lxpack writes course.yaml, assessment YAML, copies dist/, writes lessonkit.json, then calls buildCourse. LessonKit maintains parallel YAML emitters.

Proposal: Add to @lxpack/api:

import type { ExportTarget } from "@lxpack/api";

export type PackageLessonkitOptions = {
  /** Merged interchange + course metadata (see schema below). */
  interchange: LessonkitInterchangeV1;
  /** SPA payloads: lesson id → absolute path to folder with index.html */
  spaDirs: Record<string, string>;
  /** Optional in-memory assessments (same shape as buildCourse today). */
  assessments?: AssessmentInput[];
  target: ExportTarget;
  /** Output zip or directory; optional staging courseDir for debugging */
  output?: string;
  dir?: boolean;
  courseDir?: string; // default: temp; kept on failure if debug: true
};

export type PackageLessonkitResult =
  | { ok: true; outputPath?: string; outputDir?: string; fileCount: number; courseDir: string }
  | { ok: false; issues: Array<{ path?: string; message: string; severity?: "error" | "warning" }> };

export function packageLessonkit(options: PackageLessonkitOptions): Promise<PackageLessonkitResult>;

LXPack responsibilities:

  • Generate course.yaml internally from interchange (no consumer YAML emitter).

  • Copy SPA dirs with path containment (reuse existing rules).

  • Prefer only in-memory assessments for LessonKit-sourced quizzes; skip writing assessments/*.yaml unless writeAuthoringFiles: true.

  • Return structured issues (same shape as validateCourse).

Acceptance criteria:

  • examples/lessonkit-spa builds using only @lxpack/api (no checked-in course.yaml required for LessonKit path).

  • LessonKit deletes yaml.ts, assessmentYaml.ts, and most of writeProject.ts.

  • CI: golden LessonKit course packages identically before/after migration (ZIP hash or manifest comparison).

LessonKit follow-up: packageLessonkitCourse() becomes a thin wrapper mapping LessonkitCourseDescriptorPackageLessonkitOptions.


P0 — Formalize lxpackBridge as @lxpack/spa-bridge

Problem: Bridge types and score normalization live in @lessonkit/lxpack/bridge. Non-LessonKit SPAs duplicate logic; versioning is informal.

Proposal: New package (or @lxpack/runtime/bridge export):

// Host (LXPack runtime) — register once
export function createLxpackBridgeHost(options: BridgeHostOptions): LxpackBridgeV1;

// Child (SPA) — safe access from iframe
export function getLxpackBridge(): LxpackBridgeV1 | null;
export function normalizeScore(raw: { score: number; maxScore?: number }): number | null;
export function normalizePassingThreshold(raw: { passingScore?: number; maxScore?: number }): number;

export type LxpackBridgeV1 = {
  completeLesson(lessonId: string): void;
  completeCourse(): void;
  submitAssessment(payload: { id: string; score: number; passingScore?: number }): void;
  track?(event: TrackingSchemaEvent): void;
};

Document in LXPack docs:

Method

Score / threshold scale

When to call

submitAssessment

0–1 scaled

After quiz graded in SPA

YAML passingScore in author assessments

absolute points

Native LXPack quizzes only

Versioning: Keep window.parent.lxpackBridge.v1; add v2 alongside with a capability negotiation helper (bridge.supportedVersions).

Acceptance criteria:

  • Published TypeScript types on npm.

  • examples/lessonkit-spa imports child SDK from @lxpack/spa-bridge.

  • LessonKit removes duplicate bridge.ts and depends on LXPack package.

  • Validator warns when SPA index.html references deprecated window.parent.lxpack without bridge.


P1 — Own the lessonkit.json interchange schema

Problem: Schema is implied by LessonKit’s LessonkitInterchangeV1 type; LXPack merges file at build time but does not publish a versioned JSON Schema / Zod module.

Proposal:

  • Add @lxpack/validators/lessonkit (or lessonkitInterchangeSchema export).

  • Document fields:

{
  "format": "lessonkit",
  "version": "1",
  "course": { "id": "course-id", "title": "Title" },
  "lessons": [{ "id": "lesson-id", "title": "...", "type": "spa", "path": "dist" }],
  "assessments": [{ "id": "check-id", "passingScore": 1, "questions": [] }],
  "tracking": { "completion": { "threshold": 1 } },
  "runtime": { "theme": "default", "cssVariables": { "--lk-color-primary": "#2563eb" } }
}
  • validateCourse accepts interchange-only projects (generates missing course.yaml).

  • Breaking changes bump version: "2" with migration notes.

Shift from LessonKit: Stop emitting interchange by hand in descriptorToInterchange; optionally generate from descriptor in LessonKit until descriptors are deprecated.

Acceptance criteria:

  • JSON Schema published under docs/reference/lessonkit-interchange.md.

  • Invalid interchange fails validateCourse with path-qualified errors.

  • LessonKit CI imports schema from @lxpack/validators (devDependency) instead of duplicating rules.


P1 — Unified tracking map (LessonKit telemetry ↔ LXPack)

Problem: LessonKit emits lesson_completed, quiz_completed, etc. (@lessonkit/core catalog). LXPack has @lxpack/tracking-schema. Adapters guess mappings for bridge and xAPI.

Proposal:

  • Publish mapLessonkitTelemetryToLxpack(event): TrackingSchemaEvent | null in @lxpack/tracking-schema (or @lxpack/api).

  • Publish verb / activity IRI recommendations for packaged courses (xAPI export).

  • Optional: bridge.track() accepts canonical events so SPAs can forward rich telemetry without custom LMS code.

Reference mapping (starter):

LessonKit TelemetryEventName

LXPack / xAPI intent

course_started

course initialized

course_completed

course completed

lesson_started

lesson initialized

lesson_completed

completeLesson + completed verb

quiz_answered

interaction / answered

quiz_completed

submitAssessment + scored

interaction

track({ type, id, data })

Acceptance criteria:

  • Table is documented and unit-tested in LXPack.

  • LessonKit @lessonkit/react uses exported mapper (delete bespoke switch in lxpackBridge.ts).

  • xAPI ZIPs from LessonKit courses use consistent activity ids derived from interchange course.id / lesson ids.


P1 — Theme interchange without @lessonkit/themes

Problem: themeToLxpackRuntime() in LessonKit depends on @lessonkit/themes to produce runtime.cssVariables.

Proposal:

  • Interchange carries optional runtime.cssVariables (already natural in YAML).

  • LXPack applies variables to learner shell (already supports runtime.cssVariables in course.yaml).

  • Optional: runtime.themePreset: "lessonkit:brand" resolved via a registered preset table in LXPack or passed fully expanded in interchange.

Shift: LessonKit only expands presets at package time (one function), or authors commit expanded variables in lessonkit.json. LXPack does not import LessonKit packages.


P2 — Preview and validate from SPA build output

Problem: Developers run lessonkit dev (Vite) but must materialize .lxpack/course to use lxpack preview with LMS chrome.

Proposal:

lxpack preview --lessonkit ./lessonkit.json --spa dist/
  • Starts runtime with SPA lesson iframe + bridge host.

  • Optional SCORM simulator flags (existing lxpack.config.json preview modes).

Acceptance criteria:

  • Documented workflow in LessonKit interoperability guide.

  • No assessments/*.yaml required on disk when passing --assessments JSON path or embedded in interchange.


P2 — SCORM layout recipes for LessonKit

Problem: LessonKit supports single-spa (one SCO, in-app navigation) vs per-lesson-spa (multi-SCO). Authors lack LXPack guidance on SCORM 2004 sequencing for each.

Proposal:

  • Document recipes in LXPack:

    • Recipe A — Single SCO SPA (default LessonKit): one type: spa lesson; completion via bridge.

    • Recipe B — Multi SCO: one SPA folder per lessonId; flow optional; map lesson_completed per SCO.

  • Validators emit warnings when interchange lessons omit path or id collisions would break SCORM 2004.

  • Optional: buildCourse({ scormLayout: "single-sco-spa" | "multi-sco-spa" }) expands interchange automatically.

Open question for maintainers: Should multi-lesson React apps ever share one SPA build (hash router), or should LXPack enforce per-lesson builds for true multi-SCO?


P2 — Shared conformance harness (@lxpack/conformance or test/fixtures/lessonkit)

Problem: LessonKit 0.9.x plans export parity tests; LXPack risks regressions without shared fixtures.

Proposal:

  • Git submodule or npm package with:

    • Minimal interchange + tiny SPA fixture (static HTML + one quiz call to bridge).

    • Matrix: standalone, scorm12, scorm2004, xapi, cmi5.

    • Expected: launch succeeds, completeLesson marks complete, submitAssessment records score.

  • Both repos run npx @lxpack/conformance in CI.

Acceptance criteria:

  • LXPack release workflow runs conformance on tag.

  • LessonKit packaging smoke imports same package (no forked fixture copies).


P3 — Optional @lxpack/lessonkit meta-package

Problem: Consumers install @lessonkit/lxpack + @lxpack/api and must understand Node 18+, interchange, bridge.

Proposal:

  • @lxpack/lessonkit re-exports packageLessonkit, bridge SDK, schema types.

  • Peer dependency: react not required (packaging only).

  • LessonKit deprecates @lessonkit/lxpack over a major version with migration guide.


What should stay in LessonKit (non-goals for LXPack)

  • React component library and hooks (Course, Lesson, Quiz, …).

  • Authoring-time telemetry sinks and @lessonkit/xapi client (distinct from LMS-export xAPI inside LXPack).

  • Vite template, lessonkit init, lessonkit dev, lessonkit build.

  • Block catalog for AI/Studio (block-catalog.v1.json).

  • WCAG primitives (@lessonkit/accessibility).

Non-goals (both projects):

  • Merging repositories.

  • Replacing LXPack markdown/HTML authoring with LessonKit-only workflows.

  • Re-implementing SCORM manifest generation in LessonKit.


Suggested LXPack release sequence

Release

Theme

Status

v0.5.0

Thin packaging

ShippedpackageLessonkit(), interchange schema

v0.5.x

Bridge SDK

Shipped@lxpack/spa-bridge

v0.6.0

Tracking + theme

Shipped — interchange runtime, telemetry map, LessonKit integration in this repo

v0.6.x

Conformance

Planned — shared fixtures package, SCORM recipes

v0.7.0

Optional meta-package

Planned — @lxpack/lessonkit; thin @lessonkit/lxpack

Coordinate with LessonKit 0.9.x (conformance harness) and 1.0.0 (stable public API).


API stability commitments (request to LXPack)

LessonKit 1.0.0 needs these to remain stable (or versioned with migration):

  1. lxpackBridge.v1 method signatures and 0–1 score semantics for submitAssessment.

  2. lessonkit.json format + version with documented migration path.

  3. ExportTarget enum and buildCourse / packageLessonkit result shapes.

  4. SPA lesson type: spa + path pointing at folder with index.html.


How to validate with LessonKit today

Maintainers can verify changes against the LessonKit repo without adopting the full monorepo:

git clone https://github.com/eddiethedean/lessonkit.git
cd lessonkit
npm ci
npm run build
npm -w lessonkit-example-lxpack-golden run build
npm -w lessonkit-example-lxpack-golden run package:scorm12

Artifacts: examples/lxpack-golden/.lxpack/course/.lxpack/out/course-scorm12.zip

Contact surface: open issues in either repo with label interop/lessonkit; attach interchange JSON and buildCourse / packageLessonkit structured errors.


Summary for LXPack maintainers

  1. Absorb project materializationpackageLessonkit() so adapters stop writing YAML by hand.

  2. Own interchange + bridge — schema, typed SDK, explicit score semantics.

  3. Publish tracking maps — one table from LessonKit telemetry to LXPack/LRS.

  4. Ship shared conformance — both projects gate releases on the same fixture matrix.

  5. Keep LessonKit thin — React authoring only; LMS delivery stays in LXPack.


Historical checklist (LessonKit team)

What LessonKit requested before LXPack v0.4.0, and integration status from the LessonKit side:

the improvements we wanted in LXPack so it works better as the packaging and LMS export layer for LessonKit, plus what LessonKit should do next.

Status

  • LXPack v0.4.0 — baseline SPA + @lxpack/api + lessonkit.json merge (historical checklist below).

  • LXPack v0.6.0packageLessonkit(), interchange schema in @lxpack/validators, @lxpack/spa-bridge, @lxpack/tracking-schema telemetry map, interchange runtime + assessments. LessonKit 0.8.2 integrates these (^0.6.0); see maintainer upgrade plan.

LessonKit is React-first authoring (@lessonkit/react). LXPack is a manifest-driven compiler and runtime (course.yaml, markdown/HTML/component lessons, SCORM/xAPI/cmi5 export). The two projects are complementary: LessonKit owns the developer experience; LXPack owns validation, preview, and LMS artifacts.

Related LessonKit docs:


Current integration plan (LessonKit side)

LessonKit’s preferred path is Strategy A from the roadmap:

  1. Author courses in React with @lessonkit/react.

  2. Export to an LXPack project via @lessonkit/lxpack (shipped in 0.6.0 — see Packaging reference).

  3. Run lxpack validate and lxpack build --target for LMS delivery (via validateLessonkitProject / packageLessonkitCourse or the golden example scripts).

The adapter maps a LessonkitCourseDescriptor plus built SPA assets into LXPack’s course.yaml and lessonkit.json; multi-lesson UX stays in your React app for single-spa layouts.


What changed in LXPack v0.4.0 (impact on LessonKit)

Because LXPack now implements the features we previously asked for, LessonKit should treat LXPack as the default packaging toolchain and focus on building a thin, well-tested adapter.

Recommended LessonKit next steps:

  1. ~~Create @lessonkit/lxpack~~ — done (0.6.0).

  2. ~~Decide a stable mapping for identities~~ — done (identity v1; see Identity reference).

  3. ~~Add at least one end-to-end example~~ — done (](https://github.com/eddiethedean/lessonkit/tree/main/examples/lxpack-golden/)).

  4. ~~Add a CI smoke test that runs LXPack packaging for that example~~ — done (.github/workflows/checks.yml packaging job; integration; e2e).


Design goals for interoperability

Goal

Why it matters

Preserve React authoring

LessonKit users should not rewrite courses as YAML/markdown to ship to an LMS.

Stable identity model

courseId, lessonId, assessment ids must map 1:1 into tracking, xAPI, and SCORM suspend data.

Shared tracking semantics

Completion, quiz pass/fail, and time-on-task should mean the same thing in both runtimes.

Programmatic packaging

@lessonkit/lxpack needs library APIs, not only CLI subprocesses.

npm-first consumption

LessonKit uses npm workspaces; LXPack packages should install cleanly without requiring pnpm for consumers.


Gap analysis

1. Authoring model mismatch

LXPack today

LessonKit today

Declarative course.yaml + file-based lessons

JSX component tree (Course, Lesson, Quiz, …)

Lesson types: markdown, html, component

Rich React composition, custom layout, app state

Built-in widgets (callout, image-card, …)

Framework primitives + user-defined UI

Pain: Exporting LessonKit → LXPack today implies serializing React to markdown/HTML or re-implementing interactions as LXPack component lessons. That breaks fidelity and accessibility work done in React.

2. No first-class “hosted React bundle” lesson type

LXPack can package standalone web apps, but there is no documented lesson type for:

  • A Vite/React build output as a lesson SCO with known entry (index.html)

  • Wiring that lesson into flow, completion rules, and multi-SCO SCORM 2004

Pain: LessonKit’s natural artifact is a built SPA, not a folder of markdown files.

3. CLI-centric integration surface

LXPack’s primary interface is @lxpack/cli (init, preview, validate, build). LessonKit needs:

  • validateCourse(project) / buildCourse(project, target) as importable functions

  • Typed options and structured errors (for CI and @lessonkit/lxpack)

Pain: Subprocess + stdout parsing is fragile for monorepo CI and IDE integrations.

4. Tracking and xAPI vocabulary alignment

LessonKit (0.1.x) emits telemetry events and minimal xAPI statements (started, completed). LXPack has mature tracking, completion thresholds, quiz YAML, and export-time embedding.

Pain: Without a shared event/verb map, adapters guess at semantics and LRS reports diverge.

5. Assessment model differences

LXPack

LessonKit

Author YAML in assessments/; keys embedded at build

Inline Quiz / KnowledgeCheck in React

passingScore, maxAttempts, shuffle, feedback modes

Simple correct/incorrect + useQuizState hooks

Pain: Export must invent assessment YAML from React props or lose quiz metadata.

6. Theming and accessibility

LessonKit targets WCAG 2.1 AA with React semantics and @lessonkit/accessibility helpers. LXPack runtime uses markdown sanitization, HTML interactions, and runtime.theme CSS classes.

Pain: Branding and a11y behavior may differ between preview (LXPack) and author preview (LessonKit/Vite) unless theme contracts align.



Suggested division of responsibility

flowchart TB
  subgraph author [Authoring]
    LK["LessonKit React app\n(@lessonkit/react)"]
  end

  subgraph bridge [Bridge - planned]
    LKL["@lessonkit/lxpack\nexport + metadata"]
  end

  subgraph lxpack [LXPack]
    VAL["@lxpack/validators"]
    CLI["@lxpack/cli / @lxpack/api"]
    PKG["@lxpack/scorm / xapi / cmi5"]
    RT["@lxpack/runtime"]
  end

  subgraph lms [Delivery]
    LMS["LMS / LRS / browser"]
  end

  LK --> LKL
  LKL --> VAL
  LKL --> CLI
  CLI --> PKG
  PKG --> LMS
  RT --> LMS

Layer

Owner

Responsibility

Authoring UX

LessonKit

Components, hooks, a11y, Vite templates

Export adapter

@lessonkit/lxpack

Build SPA(s), emit interchange + invoke LXPack

Validation & packaging

LXPack

Schema, path containment, SCORM/xAPI/cmi5 ZIPs

Learner runtime

LXPack (+ SPA bridge)

Navigation, flow, LMS APIs, quiz engine where applicable


Phased rollout (cross-repo)

Phase

LXPack

LessonKit

1

Document SPA lesson type + bridge API (even if experimental)

Spike @lessonkit/lxpack export to static dist/

2

Ship @lxpack/api validate/build

Wire lessonkit packagelxpack build

3

Tracking schema + assessment build injection

Align @lessonkit/xapi verbs with schema

4

Plugin runtime registration

Optional: embed @lxpack/runtime navigation shell around SPA


Non-goals (for now)

  • Merging the two repos into one monorepo

  • Replacing LXPack markdown authoring with LessonKit-only workflows

  • Requiring LessonKit authors to learn full course.yaml before they can ship


Open questions for LXPack maintainers

  1. Single-SCO vs multi-SCO: Should a LessonKit Course map to one SCORM package or one SCO per Lesson?

  2. Answer keys in SPA lessons: Should quiz scoring stay in LXPack runtime only, or allow client-side scoring inside the SPA with signed/embedded config?

  3. Versioning: How should lxpackBridge.v1 evolve without breaking published LessonKit courses?

  4. npm vs pnpm: Can release CI guarantee @lxpack/* packages work as npm dependencies in LessonKit’s workspace?


Summary

LXPack already solves problems LessonKit should not rebuild (SCORM manifests, ZIP packaging, xAPI/cmi5, validation, preview). With LXPack v0.4.0 implementing the suggested features, the highest-value work for LessonKit is now:

  1. Build @lessonkit/lxpack as the packaging adapter

  2. Ship one end-to-end SCORM export example

  3. Lock down identity + tracking mappings (course/lesson/assessment ids)

That delivers LMS-ready packages without forcing authors out of React.