Skip to content

Separate document numbering from scored item numbering - #49

Merged
dqnykamp merged 4 commits into
Doenet:mainfrom
dqnykamp:feat/description-item-numbering
Aug 15, 2026
Merged

Separate document numbering from scored item numbering#49
dqnykamp merged 4 commits into
Doenet:mainfrom
dqnykamp:feat/description-item-numbering

Conversation

@dqnykamp

@dqnykamp dqnykamp commented Aug 15, 2026

Copy link
Copy Markdown
Member

Problem

isDescription marks a document as unscored and unnumbered. The question counter, credit extraction, shuffle anchoring, and the per-item attempt button all honored it already — but the item indexing did not.

getNumItems and getItemSequence counted descriptions as items, while extractActivityItemCredit excluded them. Those two indexings feed different fields of the same SPLICE.reportScoreAndState message:

  • item_scores[].shuffledOrder — scored items only
  • item_updated / new_doenet_state_idx — position in the document sequence, descriptions included

The host (DoenetApps) stores per-item state keyed by the scored numbering, and rebuilds doenetStates / itemAttemptNumbers from rows ordered by shuffledItemNumber. So with any description present, item state was written to the wrong item, and the two arrays were sized and indexed inconsistently.

Descriptions were never reachable from the host UI, so this never fired in production — but it blocks adding them, which is what DoenetApps wants to do.

Change

Split the two concepts that were sharing one sequence.

Includes descriptions? Drives
Document sequencegetDocSequence / getNumDocs yes mounting, pagination, checkHidden, checkKeepLive
Scored item sequencegetScoredItemSequence / getNumScoredItems no doenetStates, itemAttemptNumbers, item_updated, new_doenet_state_idx

The first pair is a rename of getItemSequence / getNumItems — no behavior change; the existing doc comment on getNumItems already said "the number of documents that will be rendered".

The scored ordering is the shuffled one, because the host rebuilds both arrays from rows ordered by shuffledItemNumber asc. getScoredItemSequence is implemented by filtering getDocSequence rather than by a separate recursion, which makes the subsequence invariant structural and sidesteps the slice(0, numToSelect) branch.

Other behavior:

  • A description reports nothing on state update. It holds no slot in doenetStates and its state is not persisted, so emitting a report would cost a saveScoreAndState round trip per keystroke for a row loadState ignores. The host tolerates a missing item_updated, so this is a no-op there.
  • new_attempt_for_item is unchanged. It is derived from item_scores, so it is already in original (unshuffled) order and already description-free. It legitimately differs from item_updated; a test pins that so it is not "fixed" later.
  • Descriptions inside a select now throw instead of silently miscounting. extractSelectItemCredit scores a single-document select regardless of isDescription, and the select branch of propagateStateChangeToRoot averages over all selected children without filtering. Viewer already calls this inside a try, so it surfaces as the existing "Error in activity source" banner.
  • A document with no scored slot is a no-op, never an error. Both updateSingleState and generateSingleDocSubActivityAttempt now return the state unchanged when the id is missing from the scored item sequence — whether because it is a description or because it is stale (an in-flight save from a just-regenerated attempt, or a select that re-picked its children). generateSingleDocSubActivityAttempt previously let such an id fall through and surface as an error banner.
  • Activity.tsx falls back to itemAttemptNumber 1 and no answerResponseCounts for a description, instead of reading whatever itemAttemptNumbers[-1] gave (undefinedNaN attempts-left, masked only because the button is hidden).

Compatibility

No change to ExportedActivityState, isExportedActivityState, or sourceHash — only the length and meaning of two arrays narrows.

Old saved state cannot collide with the new indexing: createSourceHash hashes isDescription, and validateStateAndSource rejects a hash mismatch, so any source containing a description necessarily has a different hash and starts fresh. Sources without descriptions compile and index exactly as before.

Tests

npm run test — 57 pass (47 pre-existing unchanged, which is itself the compatibility evidence: every description-free fixture still asserts the same item_updated, new_doenet_state_idx, and itemAttemptNumbers).

New unit tests use the existing seqWithDes.json fixture:

  • document vs scored counts, and the subsequence relation across several shuffled attempts
  • the contract test — for every i, the shuffledOrder of scoredSeq[i] in extractActivityItemCredit equals i + 1. This is the exact invariant the host's shuffledItemNumber depends on; it catches any future reimplementation that drifts.
  • a description state update is a no-op (identity-equal return, no postMessage)
  • a scored document following a description reports item_updated strictly less than its document index and equal to its own shuffledOrder
  • generateSingleDocSubActivityAttempt bumps only its own scored index, and new_attempt_for_item may differ from item_updated
  • edge cases for the description/scored split, each checked over several shuffled attempts: a bare description at the root, a sequence of nothing but descriptions, a description as the last child (so the shufflable run ends at it), descriptions on both ends, and a sequence nested in a sequence with a description in each
  • new selWithDes.json fixture → getNumScoredItems throws
  • a generateSingleDocSubActivityAttempt for a description, and one for a document not in the activity at all, are both identity-equal no-ops that set no errMsg and post no message

New Cypress component test (ActivityViewer.descriptions.cy.tsx): page count includes the description, attempt buttons exist only for scored items, the dialog on the first problem reads "problem 1" (not 2), and reported item_updated is 1 with the description absent from item_scores.

Version

Bumped to 0.1.0-alpha-18. DoenetApps needs this published before its companion PR can be merged.

`isDescription` marked a document as unscored and unnumbered, and the
question counter, credit extraction, shuffle anchoring, and per-item
attempt button already honored it. The item *indexing* did not:
`getNumItems` and `getItemSequence` counted descriptions, while
`extractActivityItemCredit` excluded them.

Those two indexings feed different fields of the same
`SPLICE.reportScoreAndState` message, and the host stores per-item state
keyed by the scored numbering. With any description present, item state
was therefore written to the wrong item.

Split the two concepts:

- Document sequence (`getDocSequence`/`getNumDocs`, renamed from
  `getItemSequence`/`getNumItems`, whose doc comment already said
  "documents") — every rendered document, descriptions included. Drives
  mounting, pagination, and hide/keep-live.
- Scored item sequence (`getScoredItemSequence`/`getNumScoredItems`) —
  descriptions excluded, in render order. Indexes `doenetStates` and
  `itemAttemptNumbers`, and produces `item_updated` and
  `new_doenet_state_idx`.

`getScoredItemSequence` filters `getDocSequence` rather than recursing
separately, which makes the subsequence invariant structural.

A description now reports nothing on state update: it holds no slot in
`doenetStates` and its state is not persisted, so emitting a report
would cost a save round trip for a row `loadState` ignores.

`new_attempt_for_item` is unchanged — it is derived from `item_scores`,
so it is already in original order and description-free.

Descriptions inside a `select` are rejected rather than silently
miscounted: `extractSelectItemCredit` scores a single-document select
regardless of `isDescription`, and `propagateStateChangeToRoot` averages
over all selected children without filtering.

Saved state cannot collide with the new indexing: `createSourceHash`
hashes `isDescription`, so a source containing a description necessarily
has a different hash and starts fresh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3bUTUym3xhuXLegDyTNZ2
dqnykamp and others added 3 commits August 14, 2026 20:08
Collapse the redundant document-sequence membership check in
`updateSingleState`: a stale id and a description both take the same
no-op path through the scored item sequence lookup.

Let `Activity.tsx` rely on the out-of-range index falling back rather
than branching on the `-1` sentinel twice, and express the select and
sequence counters uniformly.

Add a test for a new item attempt on a document that is no longer in
the activity, and pin the reported `item_scores` doc ids in the Cypress
description test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3bUTUym3xhuXLegDyTNZ2
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3bUTUym3xhuXLegDyTNZ2
Rename `itemsRendered` to `docsRendered` in `Viewer` and correct the
comments and JSDoc that still said "item" where a document (descriptions
included) is meant.

Add a test exercising the description/scored split over several shuffled
attempts for a bare root description, a sequence of only descriptions, a
description as the last child, descriptions on both ends, and a sequence
nested in a sequence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K3bUTUym3xhuXLegDyTNZ2
@dqnykamp
dqnykamp merged commit a5cd546 into Doenet:main Aug 15, 2026
4 checks passed
@dqnykamp
dqnykamp deleted the feat/description-item-numbering branch August 15, 2026 02:16
cqnykamp pushed a commit to cqnykamp/DoenetApps that referenced this pull request Aug 20, 2026
A Description checkbox on each document card in the problem set editor,
disabled (but still showing its state) once the content is read-only, and a
distinct icon (`MdNotes`, slate) so descriptions read as structurally
different from the problems around them — shape and colour, not colour
alone.

The client compiler honours `isDescription` for a direct child of a problem
set and never repeats a description, mirroring the server twin so that
every path compiles a problem set identically.

Both the Description and Repeat controls explain themselves. The text rides
on `aria-label`, not only the tooltip, so it is announced on focus rather
than being hover-only, and both share the `HoverFocusTooltip` wrapper that
owns the keyboard behaviour and overflow placement.

Bumps `@doenet/assignment-viewer` to 0.1.0-alpha-18
(Doenet/assignment-viewer#49), which is published. That release separates
the document sequence from the scored item sequence, so `doenetStates`,
`itemAttemptNumbers`, and the reported `item_updated` are keyed by scored
item — the numbering this app stores per-item state by. Descriptions depend
on it: without it, item state for a problem set containing one is written
to the wrong item.

Requires the descriptions API, which requires the `content.isDescription`
column.

Not covered: the assigned-student persistence round trip (answer, reload,
confirm the answer restores into the right problem) and the gradebook
column count were not exercised against a real assignment. The viewer's own
tests pin the numbering and reporting these depend on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant