Skip to content

Tools

Two offline validators, both run by CI, neither needing Unity or a licence.

Script Checks
validate-package.py A generated SCORM zip, the way an LMS importer would
check-site-links.py That every internal link in the built docs site resolves

validate-package.py

The offline half of the conformance gate. It opens a generated SCORM zip and checks it the way an LMS importer would, before an LMS gets the chance to reject it with a message about XML.

python3 Tests~/tools/validate-package.py path/to/package.zip [more.zip ...]

Python 3, no dependencies.

What it checks

The archive. That imsmanifest.xml exists and is at the root, not in a subfolder — the single most common reason an otherwise-correct package is refused on upload.

The manifest as XML. Well-formed, and with no UTF-8 BOM. A BOM in front of the declaration makes strict parsers reject the document, and it is invisible in every editor.

The manifest as SCORM. A recognized content-packaging namespace; a <schema> of ADL SCORM; a <schemaversion> that matches the standard the package claims — 1.2, or a real 2004 edition. It also fails a manifest still carrying the placeholder identifier, because a package that imports under a generated name is a package nobody can find again.

That the parts refer to each other. No dangling resource references, organizations/@default resolving to an organization that exists, at least one item pointing at a resource, every referenced resource declared, and a SCO present with a launch href.

That the files and the manifest agree, in both directions. Every file the manifest declares is in the archive, and every file in the archive is declared in the manifest. The second direction is the one that catches a packer that stopped including something, and also the one that catches stray files riding along.

That hrefs are usable URIs.

It runs in CI

Every push. ScormBuildSmokeTests drives the shipped ScormPostBuild.Package and writes real zips; this validates those. So the thing being checked is what a customer would upload, not a fixture that resembles it.

That wiring is newer than the script. It used to run nowhere: the "48/48" recorded in an earlier CONFORMANCE.md came from a local Builds/ folder that was never in the repository and could not be reproduced from a clone.

With no arguments it used to print 0/0 checks passed and exit 0 — a green result indistinguishable from a real one. That is the same class of failure as a test runner exiting zero having run nothing, which is why the CI step now asserts packages were found before calling this.

What it does not check

Anything that needs an LMS. A package can pass every check here and still behave wrongly once a real platform interprets it — which is why ../CONFORMANCE.md exists alongside it.

check-site-links.py

python3 Tests~/tools/check-site-links.py site

Walks the built site and checks that every internal href resolves to a file, or to a directory with an index.html. Root-absolute links have the site's mount path stripped first, which 404.html needs because it is the one page that must use absolute URLs.

It exists because mkdocs build --strict does not cover everything it looks like it covers. Strict mode fails on a dangling link written as markdown. Raw HTML is stashed during conversion and restored untouched, so an <a href="foo.md"> in a hand-authored page ships verbatim, 404s under use_directory_urls, and leaves the build green.

That is not hypothetical. The landing page's six card links were written that way, built green, and every one of them was broken. The check was confirmed against that exact bug: reintroduce one .md href and mkdocs build --strict still exits 0 while this names the page and the link.