Common Cartridge 1.4 import — implementation plan
Companion to ADR-0022. Executable, sliced build plan. Two repos move: spec-shaped decompose + QTI 1.2→3 bridge are born upstream in conform-ed ([[feedback_internal_lib_release_all_packages]]); the domain mapping, schema, endpoint, and UI are emergent. Each slice is independently shippable + validated; feature branch per slice, rebased to land ([[feedback_rebase_never_merge]]).
Grounding (verified 2026-06-15)
Section titled “Grounding (verified 2026-06-15)”- conform-ed ships the full CC 1.4 contracts (
CommonCartridgeV1_4): manifest (imscpraw/profile + thin/k12 variants), LOM metadata, discussion topic, web link, LTI (imslticc/basiclti), authorizations, curriculum-standards metadata, a11y metadata, assignment/variant/openvideo extensions, VDEX — and the CC-profiled QTI asccv1p4_qtiasiv1p2p1_v1p0.ts(QtiQuestestinteropRaw/ProfileDocumentSchema). - Engine gap:
@conform-ed/qti-xml(QtiVersion = "2.2" | "3.0.1") and@conform-ed/qti-reacthave no QTI 1.2 normalizer / 1.2→3 conversion / headless projection. There is no CC decompose runtime anywhere (only schemas; no CLI command). - emergent: ingest is item-only + offering-scoped
(ingest-service.ts writes
assessment_item+assessment_item_version, never a test). External Package kinds areh5p|cmi5|scorm-1.2|scorm-2004; runtime providerslumi|static-package|scorm-again(astatic-packageprovider exists, no matching kind). LTI schema is tool-side only (lti-tool.ts) — no consumer/launch-external path. - No CC fixtures exist (conform-ed has only the QTI-3 corpus).
Slices
Section titled “Slices”Slice 1 — QTI 1.2→3 bridge (conform-ed/qti-xml) ← LEADS
Section titled “Slice 1 — QTI 1.2→3 bridge (conform-ed/qti-xml) ← LEADS”convertCcQtiV1ToV3(questestinteropXml) → QTI 3.0.1 ASI XML, covering the CC 1.2.1 subset:response_lidSingle/Multiple/Ordered →choiceInteraction;response_str+render_fib→textEntryInteraction/extendedTextInteraction;material/mattext→ item body;resprocessing(respcondition/setvar/displayfeedback) → response processing + feedback.- Bridge round-trip test: 1.2.1 fixture → convert →
validateQtiXmlContentpasses → headless projection delivers + scores. Release single-version; pin in emergent. - Outcome: CC’s QTI becomes ingestable by emergent’s existing pipeline, unchanged.
Slice 2 — CC decompose (@conform-ed/common-cartridge, NEW)
Section titled “Slice 2 — CC decompose (@conform-ed/common-cartridge, NEW)”decomposeCommonCartridge(bytes) → DecomposedCartridge: unzip (fflate), validateimsmanifest.xmlagainst CC 1.4 contracts, return the<organizations>tree +resources[]typed by CC resource-type with hrefs + file contents; resolve resource dependencies (whichwebcontentfiles are assets of which resource).- Unit tests over vendored real cartridges asserting tree shape + resource classification.
Slice 3 — emergent schema + test-level QTI ingest
Section titled “Slice 3 — emergent schema + test-level QTI ingest”- New
web-contentExternal Package kind/launchKind;cartridge_importrecord (statuspending→ready→failed, report jsonb, provenance links). Migration via the regenerate-migrations runbook ([[feedback_never_edit_generated_migrations]]; DB reset/apply are Anton’s steps). - Test-level ingest: convert a CC
assessment→ QTI 3assessmentTestXML → validate/normalize → createassessment_definition(Assessment Test) + referenced Items. Reuses the ADR-0020 test model. (Item-only ingest is already done.)
Slice 4 — CC mapping service + async import endpoint (apps/api)
Section titled “Slice 4 — CC mapping service + async import endpoint (apps/api)”- Map
DecomposedCartridge→ new Offering + Learning Unit tree + routed content per the ADR-0022 table; QTI via the bridge + ingest,webcontent→web-contentExternal Packages (deps bundled), long-tail → quarantined report entries with preserved payloads. POST /offerings/import-cartridge(teacher-manage / org-admin authz), async, keyed oncartridge_import; integration test over a real cartridge asserting Offering / Unit tree / Item Bank / Assessment Test / report (mapped + quarantined).
Slice 5 — teacher-portal upload + report surface
Section titled “Slice 5 — teacher-portal upload + report surface”- Upload
.imscc, pollcartridge_import.status, render the import report (what mapped, what was quarantined and why). Thin UI over the Slice-4 pipeline. (Mantine MCP first.) - BUILT 2026-06-17 (commit 345d627; documents section added with the EPUB lane), synchronous v1.
CartridgeImportCardon the offerings index (sibling to “create offering”, same org-admin gate):FileInputfor the.imscc, org select, optional title override, and a report panel listing imported assessments + question banks + readings (epub/pdf documents) + web content plus a quarantine table. On success it invalidates the offerings loader and links to the new Offering. Consumes the synchronous endpoint directly — no polling, sincecartridge_import(the async status record) was deferred with the schema. Swap to poll-on-status when Slice 3’scartridge_importmigration lands. (The earlier “web content lands server-side but isn’t listed in the report” gap was closed 2026-06-17 — the teacher result type + report now carrywebContents.)
Build note (Slices 3–5). Slices 3+4 shipped (commits 0d71ef8, a0e5535, 3a7ab29) without any new schema — the importer reuses existing
offering/learning_unit/assessment_definition/assessment_itemtables and returned the report synchronously. Slice 5 (teacher-portal upload + report surface) shipped in commit 345d627.
Build note (web-content + documents + cartridge_import, 2026-06-17). The remaining schema-backed pieces are now BUILT (one migration regenerated via the runbook; DB reset/apply remain Anton’s steps):
web-contentExternal Package kind + serving runtime.webcontentresources now land asweb-contentExternal Packages on thestatic-packageruntime (bundle stored unzipped, verbatim paths) instead of being quarantined, and are served to entitled learners through a token-authed proxy into a sandboxed iframe (apps/api/.../web-content-delivery, learner-webweb-content.$assignmentIdroute). NeedsWEB_CONTENT_DELIVERY_TOKEN_SECRET. See ADR-0022 §“Implementation status”.- EPUB/PDF documents → in-app reader (commits
871912b,79c2083,de950d0). The mapping service content-sniffs each resource’s entry file (detectDocumentKind) ahead of the kind routing, so a real.epub/webcontent; unzipped-OCF EPUBs are detected (META-INF/container.xml) and repackaged into one.epub. Lands through the sameuploadDocumentseam as ADR-0026; the report gained animported.documents[]array. See ADR-0022 §“Implementation note — EPUB3 routing” and the EPUB reading plan Slice 6.cartridge_importrecord. Every import persists provenance + report + status (pending → ready → failed); the import runs synchronously (no worker yet — async/polling is the documented future flip the record already supports). Count columns:imported_assessment_count/_question_bank_count/_web_content_count/_document_count/quarantined_count(imported_document_countadded 2026-06-17 — the whole migration set was regenerated via the runbook; DB reset/apply remain Anton’s steps).
Slice 6 — conformance lane + fixtures — BUILT 2026-06-17
Section titled “Slice 6 — conformance lane + fixtures — BUILT 2026-06-17”- Vendored two real, MIT-licensed cartridge exports (instructure/common-cartridge-viewer) into
tests/conformance/fixtures/common-cartridge/with a provenanceREADME.md(source commit + SHA-256 pins; the same corpus conform-ed’s own CC lane uses, kept byte-identical). test:cc:conformance(tests/conformance/common-cartridge.conformance.test.ts): runs the real import pipeline over PGlite (real schema viapushSchema, real SQL, real QTI ingest — deterministic + offline, like the QTI lane) over each fixture and asserts the persisted Offering + Learning Unit tree, that the CC QTI ingests as real QTI 3 items (≥4: mc/tf/mr/essay), that web content lands, and thecartridge_importrecord settlesready. The decompose + QTI 1.2.1→3 bridge round-trip are gated upstream over the same fixtures by conform-ed’scc-conformance.test.ts, so this lane proves the emergent half rather than duplicating it.- Core gate — “nothing silently dropped.”
expectNothingSilentlyDroppedasserts every declared<resource>is accounted for in the import report (mapped to a native structure or quarantined-with-reason) — ADR-0022 §5 made executable. test:cc:conformance:local(cc-corpus.local.test.ts): opt-in lane over a larger directory of.imsccfiles (EMERGENT_CC_CORPUS_DIR, else the conform-ed viewer corpus), non-fatal on backlog but enforcing the totality invariant on every successful import. Locally it imports 5 real cartridges (incl. a 202-resource Canvasushistorycourse andallyworkshop, whose webcontent yields 3 content-sniffed EPUB/PDF documents) end to end with nothing dropped.- Note: the conformance lane is the real-execution integration assertion Slice 4 named — it
drives the actual import service against real cartridges. A separate container-backed (real
Postgres, via
run-integration-suite) lane was judged unnecessary on top of it; revisit only if PGlite/Postgres behavioural drift ever matters for this path.
Engine-gap policy
Section titled “Engine-gap policy”Any CC-profiled 1.2 case the bridge mishandles, or CC structure the decompose mishandles, is fixed in conform-ed and released ([[feedback_internal_lib_release_all_packages]]); emergent bumps the pin. emergent never patches CC/QTI semantics locally.
Verification
Section titled “Verification”- Per slice:
bun test(touched pkg) +bun run typecheck; UI/DB slices addbun run validate:fulland (Slice 3+/4)bun run test:integration. - Conformance bar:
bun run test:cc:conformancegreen on the vendored set;:localis the stretch.
Open questions (carry into build)
Section titled “Open questions (carry into build)”- O-1. Vendored CC fixture sourcing — 1EdTech CC examples vs real Canvas/Moodle/Blackboard exports (prefer ≥1 of each export tool, since their CC dialects differ).
- O-2.
webcontentlink rewriting — relativehrefs between bundled files must resolve inside thestatic-packageruntime; decide the base-path/serving convention. - O-3. Offering metadata seed — CC manifest LOM (title, subject, education level) should reconcile onto the Subject / Education Level registries (as OneRoster/CASE do), not free strings.
- O-4. RESOLVED 2026-06-17 (Slice 0 BUILT). Quarantine payload retention — each long-tail entry now
carries a friendly reason + a preserved
titleanddetail(weblink / LTI launch URL), surfaced in the teacher report; ridesreport_payload(no schema change). Discussion prompts / assignment instructions are intentionally left in the cartridge (not copied into the report). Analysed (with the native-landing options for the whole long tail) in cc-long-tail-landing-analysis.md.
Long tail (LTI / discussion / assignment / web-link)
Section titled “Long tail (LTI / discussion / assignment / web-link)”The quarantined long tail and what it would take to give each a native home is analysed separately in cc-long-tail-landing-analysis.md — each is a net-new product surface (no LTI consumer / discussion / assignment / external-link model exists today), so the import mapping is cheap but the destination feature is the cost. Sequenced by ROI there. Progress:
- Slice 0 — quarantine payload preservation: BUILT 2026-06-17 (O-4 above).
- web-link → native
learning_unit_external_link: import landing + teacher report BUILT 2026-06-17 (landWebLink;imported_web_link_count). - Learner discovery surface: BUILT 2026-06-17 —
GET /workflow/offerings/:offeringId/contents(offering-contentsfeature) + a “Course materials” section on the learner offering page surface the landed web-content, documents, and web-links to entitled members. Online discovery (not the sync rail — the delivery is online anyway), so no schema/sync change. All three content kinds are now learner-reachable. - assignment: BUILT 2026-06-17 (ADR-0027 — lands as a native human-scored Assessment).
- discussion: analysed (a threaded-discussion capability); not built — vendor-vs-build is open
(the forum/discussion category is mature) and gets its own dedicated
/grill-with-docssession; seecc-long-tail-landing-analysis.md§3. - LTI consumer: deferred by design (a standalone platform program).