Skip to content

Score multi-answer Numeric and Text entry questions with a single-cardinality mapping - #6265

Merged
AlexVelezLl merged 1 commit into
learningequality:unstablefrom
rtibblesbot:issue-6187-f273d3
Oct 5, 2026
Merged

AlexVelezLl merged 1 commit into
learningequality:unstablefrom
rtibblesbot:issue-6187-f273d3

Conversation

@rtibblesbot

@rtibblesbot rtibblesbot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Numeric and Text entry write cardinality="single", the first answer as <qti-correct-response>, and a <qti-mapping> of every answer (map_response).
  • Answers are read from the mapping; cardinality="multiple" items and Text items without a mapping (read as case-sensitive) open with every answer and save in the new shape.
  • Numeric map keys use JS Number#toString (1, not the issue's 1.0), the form Kolibri looks responses up by (convert.py:_format_number). Repeated, blank and unreadable numeric answers each keep their own map entry, so the editor still flags them on reopen.

References

Fixes #6187

Reviewer guidance

  • Setup: pnpm devsetup, seeded admin, "Sample Empty Exercise" (Published Channel).
  • Saved XML (IndexedDB), old items included: single, map_response, one mapped-value="1.0" entry per answer; keys 2.0 → 2, 0x10 as typed.

QA steps

  1. "Sample Empty Exercise", Questions tab: add Text entry Paris, then color, Colour, kolor with Case-sensitive on Colour only; bold a prompt word; CLOSE, reload, Edit each. Answers in typed order, only Colour ticked, bold kept, no error.
  2. "Sample Empty Exercise", Questions tab: add Numeric blank + 2, blank, 2 + 1e, 2.50 + 3 via Add acceptable answer; CLOSE. Blank rows show "Must be a valid number (e.g. 12, 0.5, -3.14)" and Incomplete; 1e none. Reload, Edit each: blank + 2, blank, 2 + 1, 2.5 + 3.
  3. "Sample Empty Exercise", Questions tab: add Numeric 2 + 2.0, then 2 + 0x10; CLOSE, Edit each. Rows in typed order; 2 and 2.0 show "Duplicate answers are not allowed"; 0x10, not 16, shows "Must be a valid number (e.g. 12, 0.5, -3.14)". After reload, 2.0 reads 2.
  4. On unstable, "Sample Empty Exercise", Questions tab: add Numeric 1, 2.5, 3 and Text entry Paris, paris, Lutèce. On this branch, same tab: Edit, CLOSE, reload, Edit each. Every answer appears in the order typed; Text answers ticked Case-sensitive.

Evidence

Numeric duplicates and invalid numbers

Step Screenshot
2 + 2.0: both flagged duplicate 2 + 2.0: both flagged duplicate
2 + 2.0 reloaded: 2, 2, both flagged 2 + 2.0 reloaded: 2, 2, both flagged
2 + 0x10 reloaded: 0x10 kept, flagged 2 + 0x10 reloaded: 0x10 kept, flagged

Text entry

Step Screenshot
Paris, bold prompt, no error Paris, bold prompt, no error
Paris reloaded Paris reloaded
Case-sensitive on Colour only Case-sensitive on Colour only
More captures (7)
Step Screenshot
2 + 2.0 reopened before reload 2 + 2.0 reopened before reload
2 + 0x10 unsaved: 0x10 flagged 2 + 0x10 unsaved: 0x10 flagged
Blank + 2: hint, Incomplete Blank + 2: hint, Incomplete
Blank only: hint, Incomplete Blank only: hint, Incomplete
2 + 1e: no error 2 + 1e: no error
2.50 + 3 2.50 + 3
One answer "Mars", unticked One answer "Mars", unticked

AI usage

Implemented with Claude Code from a plan approved on the issue, test first. I reviewed the diff; the QTIEditor Jest suite passes.

🤖 Generated with Claude Code


Written by rtibblesbot, an LLM-based coding agent.

@rtibblesbot

🟡 Waiting for feedback

Last updated: 2026-10-05 15:56 UTC

@rtibblesbot
rtibblesbot marked this pull request as ready for review October 1, 2026 13:15
@rtibblesbot
rtibblesbot requested review from a team and marcellamaki October 1, 2026 13:15
@rtibblesbot
rtibblesbot force-pushed the issue-6187-f273d3 branch 3 times, most recently from e7dd067 to f11f8b8 Compare October 1, 2026 14:40
@AlexVelezLl AlexVelezLl self-assigned this Oct 1, 2026

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please rebase

@rtibblesbot

Copy link
Copy Markdown
Contributor Author

Rebased onto unstable. Reconciling with #6150's fix (2b0f97af0, numeric answers read as authored for headless validation):

  • Numeric map keys are normalised only when they are valid xsd:double; invalid values (0x10, 1e400) are written as authored.
  • Repeated Numeric answers are no longer dropped on save, so validateQtiItem flags them as the editor does.
  • extractNumericAnswers now matches each correct-response value to one equal map-key, so repeats in the mapping read back.
  • QTIEditor Jest suites pass (1346).

for (const a of answers) {
// Trimmed to match how _extractAnswers reads <qti-value> text back.
let mapKey = a.value.trim();
const number = isString ? null : parseXsdDouble(mapKey);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was not asked in the issue. The previous behavior is the intended behavior. Values are always, always kept the same as the user input; this must not change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed the Number#toString normalization; map keys and the correct response are the author's input again (trimmed only, as before). Searched textEntry/parse.js and the serializers for other value rewrites: one place matched, now removed. Tests updated to assert as-authored keys. (f0ea4027e)

Comment on lines +232 to +241
/**
* Build a float `<qti-base-value>`. Whole numbers keep a decimal (`1.0`, not `1`) so the
* value reads as a float wherever it is written.
* Whole numbers keep a decimal (`1.0`, not `1`) so the value reads as a float wherever
* it is written.
*
* @param {number} value
* @returns {string}
*/
export function formatFloat(value) {
return Number.isInteger(value) ? value.toFixed(1) : String(value);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this does not make any difference in the QTI Schema; we can just write numbers as "1" or "1.0", right? If we introduce this function, it'd be hard to remember to use it everywhere.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, 1 and 1.0 are equivalent in the schema. Reverted formatFloat and its uses in mapping.js entirely (the whole "float literals" commit); buildFloatNode is back to its original form. Searched for other formatFloat callers: only mapping.js (4 sites), all reverted. (ff9d2dac2)

…ity mapping

- Response declaration is always cardinality="single"; correct response holds the first answer
- Every accepted answer gets a map entry, so items are scored by map_response
- Answers are read from the mapping; items saved as "multiple" still open with all answers
- A Text item with no mapping reads back as case-sensitive

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code changes look good and manual QA checks out!

@AlexVelezLl
AlexVelezLl merged commit 2580fef into learningequality:unstable Oct 5, 2026
13 checks passed
@rtibblesbot
rtibblesbot deleted the issue-6187-f273d3 branch October 5, 2026 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

QTI editor emits cardinality="multiple" and match_correct for multi-answer Numeric and Text entry questions

2 participants