How one freeze bug became a build pipeline
Stack: SCORM 1.2, Storyline 360, Moodle, Node, FFmpeg, whisper.cpp
Where it started
The freeze bug was in 04_freeze_bug.md. Twenty minutes of diagnosis once we had an LMSSetValue trace; weeks before anyone thought to add the trace. The fix shipped. Two weeks later the same shape of bug showed up in a different module — different root cause, same "the LMS is being weird" first read, same disproportionate effort to find it.
That second incident was the one that changed how I work. The fix was easy. The disproportion was the problem. We had a CMS team that could read a stack trace in production and a SCORM team that couldn't, because the SCORM toolchain ended at "publish from Storyline and upload to Moodle."
The rest of this is what I built to close that gap.
What was actually missing
I listed the recurring sources of "we're flying blind" pain over six months of postmortems. The list was longer than I expected and shorter than I feared.
- No static checks before upload. A SCORM zip with a broken manifest namespace, a duplicate
cmi.interactions.N.id, or an asset reference pointing at a file that didn't make it into the package would pass Storyline's publish step and fail silently on Moodle. The first signal would be a learner ticket two weeks later. - No accessibility gate. Every course was supposed to be "WCAG-AA-aligned." In practice that meant "the producer remembered to set
alttext most of the time." No static gate, no compile-time enforcement. - No diff between versions. A storyboard revision shipped as a re-published zip. To review what changed, somebody had to unpack both zips and
diff -rthem. Manifest changes were buried in XML noise. Nobody did this. We shipped regressions. - No local runtime. To test a SCORM change you uploaded to Moodle. Every cycle. Even for one-line CSS tweaks. The dev loop was minutes long.
- One package per language. We shipped Hindi, Tamil, Kannada as separate uploads. The gradebook saw them as separate courses. Completion didn't aggregate. Reporting was a mess.
- No runtime visibility. Once a course was live, the LMS told you completion and score. Nothing else. Was the slide janky? Did a video 404? Did a script throw? Unknown until somebody complained.
Six gaps. Each one had a workaround. None of the workarounds composed.
The pipeline I built
Each gap became a small CLI. The constraint was deliberate: small, composable, no shared state, pipeable. SCORM as a Unix-style build target.
- **
scorm-kit lint** for #1. Reads the manifest semantically, walks the asset graph, parses the launch HTML for SCORM API discovery and interaction collisions. Run pre-commit and in CI; exit code is the gate. - **
scorm-kit a11y** for #2. WCAG 2.2 AA static analysis. Catches the cheap regressions — missinglang, filename-as-alt, video without<track>, heading skips. Not a full audit; an audit deserves a human. But the static gate moves the floor from "remembered to set alt" to "can't ship without it." - **
scorm-kit diff** for #3. SCORM as a reviewable artifact. Manifest parsed semantically (identifier, schema, masteryscore — not raw XML). Asset list hashed. Text files emit unified line diffs. PR review of a SCORM package now takes minutes, not "we'll just trust it." - **
scorm-kit mock** for #4. Local HTTP server, iframe shell, fullwindow.APIimplementation. Develop withmockrunning, edit slides, refresh, see the call log. The dev loop went from minutes to seconds.--failmodes let me regression-test the wrapper's response to LMS errors without provoking a real one. - **
scorm-kit i18n** for #5. One package, N languages, learner picks at launch. Authors annotate the HTML withdata-i18n; a small runtime swaps text, media<source>s, and<track>captions. Choice persists viacmi.student_preference.language— the SCORM-standard place for it. Hindi POSH, English POSH, and the Tamil/Kannada variants now ship as a single artifact that the gradebook treats as one course. - **
scorm-kit rum** for #6. Injects a small runtime that captures nav timing, resource-load failures, JS errors, long tasks, and slide transitions. POSTs JSON beacons every two seconds, flushes onpagehide. Pairs withcmi.core.student_idas the actor (or pseudonymise upstream). For the first time we could see why a course was janky for a specific learner.
That was the original six. Two more followed once the pipeline was in production: scorm-kit privacy (sixteen-rule PII / data-leak static audit, after a procurement reviewer flagged a font CDN that set cookies in one of our compliance modules), and scorm-kit cmi5 (validator + SCORM→cmi5 wrapper, after the first enterprise RFP that required cmi5 made it clear the dual-stream pattern was the safe migration path). Eight commands now. Each one was the size that was needed to fix one specific class of pain.
What it taught me
The original instinct — "treat the SCORM zip as something to compile, validate, and review like any other build artifact" — turned out to be the whole insight. The individual tools are not novel. Linting isn't novel. Static a11y isn't novel. Diffs aren't novel. What was missing was applying these widely-understood software-engineering disciplines to the SCORM workflow, which had inherited the conventions of authoring tools (visual, monolithic, opaque) instead of the conventions of code (textual, composable, gateable).
The job title most teams reach for is Instructional Designer. The work, increasingly, is Learning Engineering — taking SCORM, cmi5, and xAPI seriously as build targets and instrumenting the pipeline that delivers them. That gap is where most of the deployed-content quality problems live, and it's where most of the value of a senior IC role lives too.
Why this is in the portfolio
The freeze-bug story is the thirty-minute version. This is the eighteen-month version. The first answers "can you debug this." The second answers "can you build something so this class of bug doesn't happen again." For the roles I'm interested in, the second is the question that matters.
scorm-kit is open source. The toolchain that runs in the Kidvento pipeline is a superset of the open-source build and includes the org-specific glue. The open-source commands are the parts that generalise.