Itembody-only QTI Subformat
QTI-Editor stores authoring state in the QTI itembody itself, so the itembody is the single source of truth for an item — including state that QTI 3.0 does not define an attribute for (e.g. an authoring correct-response, per-interaction score, per-choice scoring).
This document explains the subformat: what extra attributes the editor reads, how the final export stays valid QTI 3.0, and how to declare a new one.
When to use this
Use this document when you need to:
- understand the editor’s save/load format for a single item body
- add a new interaction attribute that must survive roundtrip export/import
- debug why a canonical attribute is stripped from the final QTI 3.0 output
The contract
- The roundtrip item-body (the editor’s own save/load format) carries non-QTI authoring attributes directly on the interaction as canonical, unprefixed names (e.g.
correct-response,score,case-sensitive,area-mappings). There are nodata-*mirrors and no marker attributes. - On final QTI 3.0 export, the composer reads those canonical attributes, folds them into standard
qti-response-declaration/qti-response-processing, and strips them from the emitted interaction. The exported item is therefore standard QTI 3.0 with no editor-specific attributes. - On import, every QTI 3.0 item is treated as third-party:
roundtripQtiItemhoistscorrect-response/scorefrom the native declarations back onto each interaction as canonical attributes, restoring the authoring state.
Where non-QTI attributes are declared
Each interaction declares its non-QTI attributes in its composer metadata:
packages/prose-qti/src/components/<name>/composer/metadata.ts
export const choiceInteractionComposerMetadata = { // ... nonQtiAttributes: [ 'score', // Object form: alias support for legacy source-name casings { source: 'correct-response', aliases: ['correctResponse', 'correctAnswer'] }, ], // ...} satisfies InteractionComposerMetadata;Entries can be:
- A plain string (e.g.
'score') — the attribute is read from the source element and stripped on final export. - An object with
{ source, mirror: false }— strip-only (themirrorfield is retained for backwards compatibility but no longer affects output, since there are nodata-*mirrors). - An object with
{ source, aliases: [...] }— additional legacy attribute names that should also be accepted on the source element and normalized tosource.
// Strip-only — preserved elsewhere (e.g. as a synthesized QTI element){ source: 'rubricScoringBlock', mirror: false }
// With legacy alias defensive net{ source: 'correct-response', aliases: ['correctResponse', 'correctAnswer'] }The compose handler reads these from the source element to populate the response declaration, then removes them from the normalized element so the final export is clean standard QTI 3.0.
Current non-QTI attribute set
| Attribute | Interactions | Recovered on import from |
|---|---|---|
correct-response | choice, extended-text, associate, hottext, text-entry, match, gap-match, order, inline-choice, select-point | qti-correct-response values of the matching qti-response-declaration |
score | all interactions above | qti-response-processing (defaults to 1) |
case-sensitive | text-entry | editor authoring hint — not reconstructed from standard QTI |
area-mappings | select-point | editor authoring hint — not reconstructed from standard QTI |
case-sensitiveandarea-mappingshave no lossless standard-QTI source, so a third-party item that never carried them imports without them.
How recovery works on import
Every QTI 3.0 item is treated as third-party. roundtripQtiItem (@citolab/prose-qti/qti3-item-import) runs a chain of idempotent transforms that hoist correct-response / score from native qti-response-declaration / qti-response-processing onto each interaction as canonical attributes. Per-type transforms run first (they carry type-specific behaviour such as the extended-text rubric source); a generic fallback then covers any remaining interaction with a response-identifier. A final roundtripItemBody transform copies identifier / title from the qti-assessment-item onto the qti-item-body.
Adding a new non-QTI attribute
To extend the subformat with a new attribute on an interaction:
- Add the attribute to
nonQtiAttributesin the interaction’scomposer/metadata.ts. Use a plain string for the simple case; the object form{ source, mirror: false }for strip-only attributes (preserved elsewhere); the object form{ source, aliases: [...] }to also accept legacy source-name casings on input. - Read the attribute in the interaction’s
.compose.tsfrom the source element and use it for whatever the interaction needs (response declaration, UI state, additional element synthesis like extended-text’s rubric block). - If the attribute must survive a third-party round-trip, ensure
roundtripQtiItemcan reconstruct it from standard QTI; otherwise document it as an editor-only hint.