Repository navigation
Conversation
A problem set had no way to include an introduction, a section header, or shared setup text: every child document was a scored, numbered problem. The viewer already had the concept — `SingleDocSource.isDescription` marks a document as unscored and unnumbered — but nothing here could set it, since both activity compilers hardcoded `isDescription: false`. Add an `isDescription` column, thread it through both compilers and the `updateContentSettings` mutation, and surface a toggle on each document card in the problem set editor. `getItemNames` skips descriptions, so they take no gradebook column and no `itemNumber` in stored item state. Changing the flag is rejected once the content is assigned, since it renumbers the items. Also fix two things this uncovered. `repeatInProblemSet` was selected only behind an opt-in flag that most callers left off, so it silently vanished from every compiled activity except the compound-editor preview — including stored revisions and their cids. Both problem-set document settings are now selected unconditionally, and the flag is gone; `isDescription` never had it, since dropping it would shift item numbering between preview and assignment. The client compiler had no repeat branch at all, so students never saw repeats regardless of how the data was selected. It now wraps a repeated document in the same synthetic `select` the server builds, and the server now emits `title` on a document as the client already did, so every path compiles a problem set identically. Delete the dead `activityJson` field. It was declared on `QuestionBank` and `ProblemSet` and read by four loaders, but the API never populated it — those loaders always took the `compileActivityFromContent` fallback. The loader result key of the same name, and RawViewer's by-cid branch that parses a genuinely stored revision source, are unchanged. Requires @doenet/assignment-viewer 0.1.0-alpha-18, which fixes the item indexing that descriptions depend on. Do not merge before that is published and the dependency bumped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3bUTUym3xhuXLegDyTNZ2
`getItemNames` labels the gradebook columns indexed by `itemNumber`, so it must line up one-to-one with the items of the compiled activity. It expanded a question bank into one name per selection but gave a repeated document a single name — harmless while repeats were being dropped from compiled activities, wrong now that they survive. Expand repeats the same way, and share one helper across the three cases. `getAssignmentResponseStudent` now selects `repeatInProblemSet` on its children so its call site agrees. Also: treat `repeatInProblemSet: 0` as a value to clamp rather than as "absent", extract the shared hover/focus tooltip in `Card.tsx`, and add component tests for the Description control. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3bUTUym3xhuXLegDyTNZ2
The assignment viewer throws on a select whose items include a description, and two reachable states produced one: a document with both the Description checkbox and Repeat set, and a description moved into a question bank, which keeps the flag. Both compilers now honor isDescription only when the document's parent is a problem set, and never wrap a description in the repeat select. The editor hides the Repeat control on a description. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3bUTUym3xhuXLegDyTNZ2
Both problem-set document settings live on the document row and travel with it when the document is moved or copied, so the compilers cannot assume they are absent outside a problem set: a repeated document moved into a question bank compiled to a select inside the bank, producing more items than the bank's numToSelect or the gradebook's columns account for. Apply the repeat only to a direct child of a sequence, as isDescription already is, and offer the Repeat control only there. numVariants also moves independently of the repeat that was clamped against it, via a source edit or revertToRevision, and the editor hides the repeat control once a document has a single variant. Cap the repeat at numVariants at compile time, sharing repeatCountInProblemSet between the compiler and getItemNames so the gradebook columns keep matching the items. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3bUTUym3xhuXLegDyTNZ2
…-only Move `repeatCountInProblemSet` into `packages/shared` so the two activity compilers and `getItemNames` derive the item count from one definition instead of twinned copies, and pin it with unit tests. Disable the Repeat control on read-only content, as the Description checkbox already is: the server rejects either change once the content is assigned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3bUTUym3xhuXLegDyTNZ2
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. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3bUTUym3xhuXLegDyTNZ2
|
/deploy-dev |
|
❌ dev3 deploy of |
1 similar comment
|
❌ dev3 deploy of |
|
/deploy-dev |
|
✅ Deployed dev3 is shared: the next push to |
`CompoundActivityEditor` shares one fetcher with its popups, and `CopyContentAndReportFinish` treats any 200 on it as its own copy result, reading `data.newContentIds`. A settings save landing there set `newContentIds` to `undefined`, which passes the `=== null` guard and crashes the render on `.length`. The copy modal is mounted whenever `addTo !== null`, which "Items from Explore" sets before navigating — so clicking a document's Description checkbox during that window crashed the editor. Only reproducible where the navigation is slow enough to leave the window open. The Description checkbox is on every document card, which made this easy to hit; `updateRepeatInProblemSet` shared the same exposure but needs a multi-variant document to appear. Route both settings updates through their own fetcher so their responses never reach the popups, and have the copy modal accept only a response that actually carries an array `newContentIds` — that guard covers all six of its call sites. `MoveCopyContent` has the same assumption and the same latent crash. It is no longer reachable from a settings save, and is left alone here: distinguishing its own response needs the Move action's shape. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3bUTUym3xhuXLegDyTNZ2
`MoveCopyContent` treated any 200 on the shared fetcher as its own result: it reported the action finished, and on the copy path read `data.newContentIds.length`, crashing when the response was another submission's. Same shape as the crash in `CopyContentAndReportFinish`. That modal could recognize its own response by the presence of `newContentIds`, but `moveContent` returns no payload, so a move's success is indistinguishable from any other 200. Track who started the submission instead: arm a ref immediately before each submit, consume it in the effect, and clear it whenever the modal opens. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3bUTUym3xhuXLegDyTNZ2
|
/deploy-dev |
|
✅ Deployed dev3 is shared: the next push to |
`ADD COLUMN` on `content` is a full table rebuild — its two FULLTEXT indexes rule out instant DDL — measured at 7 min 3 s on dev3 against a 5-minute deploy timeout. Letting the deploy run it kills the migration partway, and because MySQL DDL is not transactional that leaves Prisma's bookkeeping unfinished and blocks every later deploy, rollback included. Records the procedure for applying it out-of-band instead: the pre-flight checks and their results on prod, the SQL, the `_prisma_migrations` row with a checksum verified against the one Prisma wrote on dev3, the verification gate, and how to back out. Also records the review of documents with `repeatInProblemSet > 1`, which this PR makes take effect for students for the first time: of 15, six are soft-deleted and three sit in assigned problem sets, all experimental, so no reset is needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01K3bUTUym3xhuXLegDyTNZ2
|
Nice. The AI-written PR body is a bit hard to follow, but I think I got the idea. There are really 3 separate changes here, and I would split them into separate PRs.
This is especially true since Descriptions has to be deployed strangely. I'm closing this PR, but feel free to create new PRs for each part. |
|
Split into seven PRs, with backend and frontend separated per expand-migrate-contract. Each is safe to deploy on its own. Open now (independent of each other, each shows only its own diff):
Held until their parent merges, branches pushed to
Deploy order and the two manual steps: the pre-flight The split is content-identical to this branch. The only deltas: two mock payloads in the popup tests now use 🤖 Generated with Claude Code |
Before deploying
Two manual steps, both of which will bite on prod if skipped. Full procedure, with the pre-flight results already gathered from prod, is in
MANUAL_MIGRATION_PLAN.md.1. Apply the migration out-of-band, before the image.
ADD COLUMNoncontentcannot use instant DDL — its two FULLTEXT indexes force a fullALGORITHM=COPYrebuild, measured at 7 min 3 s on dev3 against a 5-minute deploy timeout. The deploy kills it mid-rebuild, and because MySQL DDL is not transactional the column commits while Prisma's bookkeeping does not, leaving_prisma_migrationsunfinished. Every subsequent deploy then fails withP3009— including a rollback tomain— until someone runsprisma migrate resolveby hand. This already happened on dev3 (2026-08-15 02:30 UTC). Prod'scontentis larger, so expect longer, with writes blocked for the duration. Apply the migration on its own first, mark it applied, then deploy the image. Tracked in #3027.2. Check for repeats on live assignments.
SELECT COUNT(*) FROM content WHERE repeatInProblemSet > 1— a problem set assigned before this deploy with a repeated document gains items on the next student load, and its gradebook columns shift with them. Reviewed on prod 2026-08-17: of 15, six are soft-deleted and three sit in assigned problem sets, all experimental — no reset needed. Re-check before deploying if time has passed. See the behavior-change note below.Descriptions
A problem set had no way to include an introduction, a section header, or shared setup text — every child document was a scored, numbered problem.
The viewer already had the concept:
SingleDocSource.isDescriptionmarks a document as unscored and unnumbered, and it was implemented for question numbering, credit, shuffle anchoring, and the per-item attempt button. Nothing here could set it — both compilers hardcodedisDescription: false.This adds:
content.isDescriptioncolumn + migration (one line, matching20250804195804_problem_set_repeat_doc)returnContentSelect→processContent→ both compilersisDescriptiononupdateContentSettings, with the same three gates asrepeatInProblemSet: the all-undefined no-op, thesingleDocwhose parent is asequencecheck, and the assigned-content guard. It is on the forbidden list when assigned, because changing it renumbers the items and existing attempts' storeditemNumbers would no longer line up.getItemNamesskips descriptions, so they take no gradebook column and noitemNumberin stored item stateisDescriptionandrepeatInProblemSetonly for a document whose parent is a problem set, and never repeat a description. The viewer throws outright on aselectwhose items include a description ("The case where a select contains a description is not implemented"), and both states are reachable from stored data:moveContentcarries the flag when a description is dragged into a question bank, and the Repeat and Description controls could otherwise both be set on one document. The editor also hides the Repeat control on a description.MdNotes, slate) so descriptions read as structurally different from the problems around them — shape and colour, not colour aloneBoth the Description and Repeat controls now explain themselves. The text rides on
aria-label, not only the tooltip, so it is announced on focus rather than being hover-only; the tooltips are controlled because Chakra binds its focus handlers to the Checkbox's label wrapper while focus lands on the inner input, so an uncontrolled tooltip never opens for a keyboard user. Both share oneHoverFocusTooltipwrapper inCard.tsxthat owns that behaviour and the overflow placement.Bugs this uncovered
repeatInProblemSetwas silently dropped almost everywhere. It was selected only when a caller passedincludeRepeatInProblemSet: true, and most did not — includingcreateContentRevision, so it vanished from stored revision sources and their cids. Only the compound-editor preview ever saw it. Both problem-set document settings are now selected unconditionally and the flag is deleted.isDescriptionnever went behind it: dropping it would shift item numbering between preview and assignment, which is exactly the corruption this PR exists to avoid.A repeat escaped the problem set. Both settings live on the document row and travel with it —
moveContentandcopyContentcarry the whole row — so a document repeated in a problem set keepsrepeatInProblemSet > 1after being moved into a question bank. The server compiler's repeat branch was not scoped to a problem set, and once the field is selected unconditionally that compiles aselectinside the bank, producing more items than the bank'snumToSelectaccounts for and thangetItemNameslabels. Both compilers now apply the repeat only to a direct child of asequence, exactly as they doisDescription, and the editor offers the Repeat control only there (it always did the update call, which the server then rejected). The Repeat control is also disabled on read-only content, matching the Description checkbox — it was previously left editable on an assigned problem set, where every change failed withCannot change assigned content.A repeat could outlive its variants. The repeat is clamped to
numVariantswhen it is set, butnumVariantsmoves independently afterwards — editing the source down to fewer variants, orrevertToRevision, which writesnumVariantsstraight to the row. A stalerepeat = 3on a now-2-variant document asked the viewer for three distinct variants of two, and the editor shows the Repeat control only for multi-variant documents, so a document that fell to one variant could not be corrected at all. Both compilers now cap the repeat atnumVariantsat compile time, andgetItemNamesuses the same cap so the gradebook columns still match. The rule lives once, inpackages/shared/src/logic/problemSetItems.tsasrepeatCountInProblemSet— it also folds in the description rule, and both compilers andgetItemNamesare its only callers, so the item count derived for the gradebook cannot drift from the compiled activity.Item names did not account for repeats.
getItemNamesproduces the gradebook column labels indexed byitemNumber, so it must line up one-to-one with the items of the compiled activity. It expanded a question bank into one name per selection but gave a repeated document a single name — harmless while repeats were being dropped, wrong the moment they compile. It now expands a repeat the same way (Problem (1),Problem (2)), skips descriptions, and shares onerepeatedItemNameshelper across all three cases instead of three copies of the same loop.getAssignmentResponseStudentselectsrepeatInProblemSet,isDescription, andnumVariantson its children so its own call site gets the same answer.Students never saw repeats at all, regardless of data selection, because the client compiler had no repeat branch. It now wraps a repeated document in the same synthetic
selectthe server builds, and the server now emitstitleon a document as the client already did — so every path compiles a problem set identically.The
titlegap was closed on the server side deliberately: the server-compiled source only feeds revision cids, so no student'ssourceHashchanges. Removingtitlefrom the client instead would have invalidated saved state for every problem set.Dead code removed
activityJsonwas declared onQuestionBankandProblemSetand read by four loaders, but the API never populated it — every one of those loaders always took thecompileActivityFromContentfallback. Git history shows why: it began as a client-side local, and a later PR added the "stored revision source" fallback shape and the type field without ever implementing the server half.Deleted the field and collapsed the four fallbacks. Kept the loader result key of the same name (that is the real compiled source) and RawViewer's by-cid branch, which parses a genuinely stored revision source and whose
isActivitySourceguard is load-bearing.Editor crash fixed
CompoundActivityEditorshares one fetcher with its popups, andCopyContentAndReportFinishtreats any 200 on it as its own copy result, readingdata.newContentIds. A settings save landing there setnewContentIdstoundefined, which passes the=== nullguard and crashed the render on.length.The copy modal is mounted whenever
addTo !== null, which "Items from Explore" sets before navigating — so clicking a document's Description checkbox during that window crashed the editor. Found on dev3, where the navigation is slow enough to leave the window open; it does not reproduce locally.Both settings updates now use their own fetcher, so their responses never reach the popups, and the copy modal accepts only a response actually carrying an array
newContentIds— a guard that covers all six of its call sites.MoveCopyContentshared the same assumption and the same latent crash — reporting the action finished on any 200, and crashing on the copy path readingdata.newContentIds.length. It could not use the same guard, becausemoveContentreturns no payload and so a move's success is indistinguishable from any other 200. It now tracks whether it started the submission: a ref armed immediately before each submit, consumed in the effect, cleared whenever the modal opens.Tests
npm test -w @doenet-tools/api— 387 pass. New:isDescriptionupdate rules including thatfalseis a real value rather than a no-op; a description compiling into the activity without being a scored item; a regression test thatrepeatInProblemSetsurvives into the compiled activity from a plaingetContentcall (verified adversarially — with the select removed it fails withexpected 'singleDoc' to deeply equal 'select'); a description with a repeat compiling to a plain document rather than a select; a repeat capped atnumVariantsafter the document is edited down (in the compiled source and in the item names); a repeated document moved into a question bank compiling to a plain document; and an assignment-level test that the item names of a problem set of description + repeated problem + question bank come out as["Problem (1)", "Problem (2)", "Bank"].apps/app/src/utils/activity.cy.tsx— 9 tests covering the client compiler: title and description flag,isDescriptiondefaulting, the repeat wrapper's exact shape, thatrepeat = 1still compiles to a baresingleDocso existing source hashes are preserved, that a description is not repeated, thatisDescriptionandrepeatInProblemSetare both ignored outside a problem set, and that the repeat is capped at (and dropped below)numVariants. The server-side assertions mirror these and each suite's comments point at the other, so the twin compilers cannot drift silently.apps/app/src/widgets/Card.cy.tsx— 9 tests for the two problem-set controls. Description: absent unless the document is in a problem set, reports check and uncheck to the callback, disabled but still showing its state when there is no updater (assigned content), and the description icon's accessible name plus an axe pass. Repeat: absent outside a problem set, absent on a description, disabled but still showing its value without an updater, and reporting a new value to the callback.packages/shared/src/logic/problemSetItems.test.ts— 5 tests pinningrepeatCountInProblemSetdirectly: the default of one, the plain repeat, thenumVariantscap in both directions, the floor of one, and a description never repeating.Each fix was checked adversarially: reverting the
inProblemSetgate, thenumVariantscap, the description rule, or the item-name expansion each makes exactly the intended test (and only that test) fail.Verified end to end
Against a running dev stack, a problem set of description + one problem + one problem repeated 3× rendered:
Five documents — the description (unnumbered) plus four numbered problems, with numbering continuing across the repeat. Before this, it was three documents and the repeat was dropped.
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.