QTI 3 TypeScript 0.9.5 adds secure delivery, PNP, and testable support
qti3 0.9.5 adds candidate-safe XML, server-side submissions, PNP resolution, package inspection, conformance fixtures, accessibility proof metadata, and a style-neutral Web Component player.
We published @longsightgroup/qti3-core and @longsightgroup/qti3-player 0.9.5 after a month of work on the QTI 3 TypeScript stack. The packages now cover parsing, candidate-safe delivery XML, scoring, PNP resolution, package inspection, fixtures, conformance checks, and accessibility proof metadata.
The repository stays item-focused: host products own the runner, navigation, attempt policy, identity, analytics, gradebook integration, and LMS shell, while the qti3 packages own QTI item mechanics. The packages parse the XML, validate the item, render one item, capture responses, score supported response processing, serialize attempt state, and expose proof that supported interactions behave the way the package claims.
Packages now split the work
The published packages now split the work across clear boundaries:
@longsightgroup/qti3-coreparses, validates, scores, serializes state, prepares candidate-safe XML, and handles server-side submission mechanics.@longsightgroup/qti3-playerrenders a style-neutral native Web Component and emits host-readable events.@longsightgroup/qti3-fixturespublishes synthetic QTI item fixtures and expected scoring outcomes.@longsightgroup/qti3-conformanceruns fixtures through parsing, validation, scoring, diagnostics, and expected-state checks.@longsightgroup/qti3-a11ypublishes keyboard, focus, accessible-name, validation-message, and assistive-technology proof metadata.@longsightgroup/qti3-pnpnormalizes and resolves host-supplied QTI 3 Personal Needs and Preferences data.@longsightgroup/qti3-cligives CI and release jobs commands for validation, scoring, package inspection, support reporting, fixture runs, and accessibility proof.
The core and CLI packages keep zero third-party runtime dependencies. The player depends on the core and fixtures packages, but it stays framework-neutral. React and Preact adapters exist as wrappers around the native element instead of becoming the rendering model.
Supported means tested
We call an interaction supported only when it parses into the typed model, validates against its contract, renders in the browser player, scores in the core runtime, ships with a public fixture, passes conformance checks, includes accessibility metadata, and runs through browser tests.
The current support matrix covers the public QTI 3 item interaction set: choice, text entry, extended text, gap match, hotspot, hottext, inline choice, match, order, graphic order, associate, graphic associate, graphic gap match, media, position object, select point, slider, upload, drawing, end attempt, and Portable Custom Interaction as a host contract. Deprecated qti-custom-interaction parses for an explicit diagnostic, but the runtime does not target it.
Product teams need testable support claims. Downstream systems can run the support matrix in CI.
qti3 support-matrix
qti3 assert-support
qti3 run-fixtures
qti3 a11y-proof
Core prepares delivery and submissions
@longsightgroup/qti3-core now gives hosts server-side APIs for item delivery. A host can prepare candidate-safe XML before the browser sees an item, then score submissions against authoritative XML on the server.
import {
materializeQtiItemSubmission,
prepareQtiDeliveryXml,
} from "@longsightgroup/qti3-core";
const delivery = prepareQtiDeliveryXml(authoritativeItemXml, {
mode: "static",
});
if (!delivery.ok) {
throw new Error(delivery.diagnostics.map((item) => item.message).join("; "));
}
sendToCandidate(delivery.candidateSafeXml);
const submission = materializeQtiItemSubmission({
itemXml: authoritativeItemXml,
existingState: priorAttemptState,
trustedResponses: { RESPONSE: "A" },
});
if (!submission.ok) {
throw new Error(submission.diagnostics.map((item) => item.message).join("; "));
}
storeAttempt({
state: submission.state,
responses: submission.responseVariables,
outcomes: submission.outcomeVariables,
disposition: submission.scoringDisposition,
});
prepareQtiDeliveryXml() removes answer-bearing material for static delivery, including correct responses, response processing, mappings, lookup tables, declaration defaults, and feedback subtrees. It fails closed when static redaction cannot safely represent the item. materializeQtiItemSubmission() gives hosts a reusable submission path: response validation, trusted response application, response processing, attempt-state materialization, and a scoring disposition.
Core enforces required responses
Version 0.9.5 fixes required-response behavior. validateQtiResponseVariables() now enforces required="true" when an interaction does not author explicit minimum response counts or correct responses, and malformed required attributes produce boolean authoring diagnostics.
import {
parseQtiXml,
validateQtiResponseVariables,
} from "@longsightgroup/qti3-core";
const parsed = parseQtiXml(authoritativeItemXml);
if (!parsed.ok || !parsed.document) {
throw new Error("Invalid QTI item XML.");
}
const validation = validateQtiResponseVariables({
item: parsed.document.item,
responses: { RESPONSE: "A" },
allowedUndeclaredResponseIdentifiers: ["duration"],
});
if (!validation.ok) {
for (const diagnostic of validation.diagnostics) {
console.error(diagnostic.code, diagnostic.responseIdentifier);
}
}
validateQtiResponseVariables() uses the same response-validation policy as the browser player. It checks required responses, cardinality shape, min and max response counts, association bounds, and per-choice match-max limits before a host accepts or scores a submission.
Adaptive and PNP stay host-owned
Adaptive delivery now has a server API. The server keeps authoritative XML and attempt state, processes a submitted turn, and returns a candidate-safe view for the next browser render.
import { processQtiAdaptiveItemTurn } from "@longsightgroup/qti3-core";
const turn = processQtiAdaptiveItemTurn({
itemXml: authoritativeItemXml,
priorState: savedAttemptState,
trustedResponses: { RESPONSE: "A" },
});
if (!turn.ok) {
throw new Error(turn.diagnostics.map((item) => item.message).join("; "));
}
await player.loadXml(turn.candidateSafeXml);
player.restore(turn.state);
PNP follows the same boundary. The package accepts PNP data from the host, normalizes it, resolves it against player capabilities and QTI catalog support, and reports diagnostics. The host still fetches, stores, authorizes, and transmits preference records.
import {
createDefaultQti3PnpCapabilities,
normalizeQti3Pnp,
parseQti3PnpXml,
resolveQti3Pnp,
} from "@longsightgroup/qti3-pnp";
const parsed = parseQti3PnpXml(pnpXml);
const normalized = normalizeQti3Pnp(parsed);
const resolution = resolveQti3Pnp(normalized.profile, {
capabilities: createDefaultQti3PnpCapabilities(),
qti: { catalogResolution },
activity: { language: "en" },
});
applyDisplayOptions(resolution.display);
requestCatalogSupports(resolution.catalogRequests);
Identity, consent, persistence, service access, and institutional policy remain in the LMS or delivery system that owns the candidate relationship.
The player gives hosts stable hooks
The Web Component still renders one item at a time, but hosts can now do more around that item. They can resolve package assets, resolve item stylesheets, read companion materials, opt in to candidate-specific keyword emphasis, validate locale files, query stable interaction regions, and listen for response, validation, score, and state events.
import { defineQtiAssessmentItemPlayer } from "@longsightgroup/qti3-player";
defineQtiAssessmentItemPlayer();
const player = document.querySelector("qti-assessment-item-player");
await player?.loadXml(candidateSafeXml, {
status: "interacting",
resolveAsset: (url) => packageAssetUrlFor(url),
resolveStylesheet: (stylesheet) => ({
href: packageStylesheetUrlFor(stylesheet.href),
}),
sessionControl: {
validateResponses: true,
showFeedback: false,
},
});
const regions = player?.getInteractionRegions();
const companionMaterials = player?.getCompanionMaterialsResolution();
The player APIs keep product chrome outside the item renderer. The player exposes enough structure for LMS sidebars, overlays, analytics, accessibility review, package-backed media, and local styling without taking over the whole assessment application.
Next: certification evidence
The May post introduced the idea: a strict TypeScript QTI 3 core and native Web Component player. Version 0.9.5 gives delivery systems a server delivery path, stricter response validation, adaptive turn processing, PNP resolution, QTI package inspection, machine-readable support evidence, and CI-friendly release checks.
Certification evidence against official content is next. The project already has the pieces for that work: public fixtures, external 1EdTech smoke tests, package inspection, browser tests, accessibility proof metadata, and a release command that can assert the support matrix before publishing.
The repo is public at github.com/LongsightGroup/qti3. You can run the manual harness at longsightgroup.github.io/qti3.