Repository navigation
QTI editor emits cardinality="multiple" and match_correct for multi-answer Numeric and Text entry questions #6187
Description
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Target branch: unstable
Observed behavior
When a Numeric or Text entry question in the new QTI editor has more than one accepted answer, the generated XML can't be answered correctly:
- The response declaration uses
cardinality="multiple", but<qti-text-entry-interaction>is a single input field and can only bind to acardinality="single"variable. - Response processing always uses the
match_correcttemplate. Withcardinality="multiple",match_correctexpects the submission to contain all correct values together (like a checkbox group), which a single input can never deliver.
Numeric question with accepted answers 1, 2, 3:
<qti-response-declaration identifier="RESPONSE" cardinality="multiple" base-type="float">
<qti-correct-response>
<qti-value>1</qti-value>
<qti-value>2</qti-value>
<qti-value>3</qti-value>
</qti-correct-response>
</qti-response-declaration>
...
<qti-item-body>
<p>Test</p>
<p><qti-text-entry-interaction response-identifier="RESPONSE" expected-length="50"/></p>
</qti-item-body>
<qti-response-processing template="https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct"/>Text entry has the same problem: it already writes a <qti-mapping>, but it's still scored with match_correct, so the mapping is never used.
Relevant code:
shared/views/QTIEditor/interactions/textEntry/Descriptor.js→getResponseDeclarationSchema: cardinality becomesmultiplewhen there is more than one answershared/views/QTIEditor/serialization/assembleItem.js→buildResponseProcessingNode: always writesmatch_correct
Errors and logs
N/A. Nothing errors at runtime; the generated XML is semantically invalid for the interaction.
Expected behavior
Every Numeric and Text entry question, whatever its answer count, emits cardinality="single" + <qti-mapping> + map_response:
<qti-correct-response>holds the first accepted answer.- Each accepted answer becomes a
<qti-map-entry>withmapped-value="1.0"; any other response scoresdefault-value="0.0".
Decision: a single-answer question also gets the mapping and map_response; we don't switch to match_correct for one answer. This keeps a single answer's case-sensitive flag and scores it the same way as several answers.
<qti-response-declaration identifier="RESPONSE" cardinality="single" base-type="float">
<qti-correct-response>
<qti-value>1.0</qti-value>
</qti-correct-response>
<qti-mapping default-value="0.0">
<qti-map-entry map-key="1.0" mapped-value="1.0"/>
<qti-map-entry map-key="2.0" mapped-value="1.0"/>
<qti-map-entry map-key="3.0" mapped-value="1.0"/>
</qti-mapping>
</qti-response-declaration>
...
<qti-response-processing template="https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response"/>Text entry follows the same shape with base-type="string" and keeps the per-answer case-sensitive it already writes.
User-facing consequences
Learners answering a Numeric or Text entry question with several accepted answers are always marked incorrect, even when they enter one of the accepted answers. Authors get no warning that the question is unanswerable.
Steps to reproduce
- Open a channel and create or edit an exercise with the new QTI editor.
- Add a Numeric question with a prompt and three accepted answers: 1, 2, 3.
- Inspect the generated item XML: the response declaration is
cardinality="multiple"and response processing ismatch_correct. - Repeat with a Text entry question with two or more accepted answers. You get the same result, plus a
<qti-mapping>that is never used.
Context
- Studio
unstable@1cbdce8dd - Browser-independent: the problem is in XML serialization, not rendering
Acceptance Criteria
- Numeric and Text entry questions always emit
cardinality="single"on the response declaration, regardless of answer count -
<qti-correct-response>holds only the first accepted answer - Every question, including single-answer ones, emits a
<qti-mapping>with one<qti-map-entry mapped-value="1.0">per accepted answer anddefault-value="0.0", plusmap_responseresponse processing (nomatch_correct) - Numeric questions emit a
<qti-mapping>too (today they skip it) - Text entry keeps per-answer
case-sensitiveon its map entries - Items already saved with
cardinality="multiple"still open in the editor with all their accepted answers, and are rewritten in the corrected shape on save - Tests cover single- and multi-answer serialization for both question types, plus the parse → serialize round trip
AI usage
I used Claude Code to draft this issue from my description of the bug and the expected QTI structure. Claude located the relevant serialization code, and I reviewed and edited each section.

Activity
rtibblesbot commentedon Sep 30, 2026
Plan: Numeric and Text entry questions always write
cardinality="single"; several accepted answers are scored by a<qti-mapping>(map_response), one answer bymatch_correct.Assumptions — the issue did not settle these; say so if one is wrong.
1,0), not1.0/0.0as in the issue's example; if strict string match is wanted,Mapping.getXMLneeds a float formatter and ~3 existing assertions change.caseSensitive: truestill gets a mapping (map_response), against "one answer →match_correct"; without it the flag reads back false after reopen.match_correct; ifqti-matchcompares strings case-sensitively at delivery, Text entry must always write a mapping (one condition inbuildTextEntryInteractionXML).convert.pystill writesmultiplefor converted legacy questions; left unchanged as the issue names only the editor, so those items stay unanswerable until opened and saved in the editor.TEXT_ENTRY_DECLARATION_WITH_MAPPING"ignores unmatched entries" test changes: mapping is now authoritative, so that test's old behavior is intentionally replaced.@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
rtibblesbot commentedon Sep 30, 2026
@AlexVelezLl Blocked — these cannot be assumed either way:
match_correct, but that makes a lone case-insensitive answer ("Paris" vs "paris") and trailing-whitespace answers score differently than onunstable. Keep the AC (accept the regression) or always write a mapping for Text entry (deviates from the AC)?@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
AlexVelezLl commentedon Oct 1, 2026
Hey @rtibblesbot, yes, we should always have the mapping, even for questions with one answer and numeric ones, for consistency. I have updated the issue; please go ahead. Still support incoming xml's with only the correct response declared without mapping, and consider that as case-sensitive= "true".
rtibblesbot commentedon Oct 5, 2026
@AlexVelezLl Blocked — these cannot be assumed either way:
1.0,.5,1e3) per the "values always kept as user input" request, butconvert.py:260-263says Kolibri looks map keys up inNumber#toStringform, so a learner typing1would not match an answer typed1.0. Keep raw keys, or normalise onlymap-key?Written by rtibblesbot, an LLM-based coding agent.
AlexVelezLl commentedon Oct 5, 2026
Hey @rtibblesbot, let's keep raw keys. If Kolibri doesn't treat map keys for float values the way the correct response does, could you open an issue in Kolibri so it also normalizes map keys for numeric answers?