Rubric AI feedback delivery modes - #8602
Merged
Merged
Conversation
- settings between publish immediately on answer, publish on finalise, generate draft (current, default), no feedback
… payload actions - AUTOGRADE_RUBRIC_SUCCESS can carry comments or grades, or both - published immediately comments render without refresh - fix feedback rating leakage to students
adi-herwana-nus
force-pushed
the
adi/rubric-grading-auto-publish
branch
from
September 26, 2026 10:38
ea755b3 to
73d2caa
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Motivation
Rubric AI grading could only produce draft comments: every generated comment waited for a staff member to accept it before the student saw it. For large cohort sizes, this becomes unmanageable for staff to individually click through, rate, and approve each comment.
This PR gives rubric grading similar controls to how Codaveri feedback is delivered
(
none/draft/publish).Rubric grading also needs a mode Codaveri feedback does not. Rubric answers are graded both when a student submits a
single answer and when they finalise the whole submission, so "publish" comes in two variants: publish as soon as the student submits an answer, and publish when the student finalises their submission (and all answers within it)
Change
1. The setting
Course Settings > Assessments > AI Rubric Grading > Feedback comment delivery is a radio group, like its
Codaveri counterpart:
draftpublish_on_finalisepublish_on_answer_submitnoneThe setting lives in the assessments component settings hash (no migration), defaults to
draft, and isvalidated against
AiGeneratedPostService::FEEDBACK_WORKFLOWS. It is an ordinary assessment setting thatcourse managers and owners can change. It is not behind the system-admin gate that guards the model
configuration, because it is a teaching decision rather than a cost or provider lever.
How it is implemented.
AiGeneratedPostServiceis the single place both producers of AI comments gothrough (
RubricAutoGradingServiceandApplyEvaluationsJob), so it decides everything:nonereturns before any post is created. Unlike Codaveri'snone, this cannot skip the whole job, becausethe same LLM call also produces the grade.
publishedand the topic is not marked pending, so thesecomments never enter the staff pending queues.
new comment instead of rewriting one the student may already have read.
publish_on_finaliseneeds two steps. The finalise callback runs before the grading jobs it triggers, soat that point the comments for those answers do not exist yet:
When a comment is created, it is published if the submission is no longer being attempted.
When the submission is finalised,
Submission#publish_ai_generated_feedbackpublishes the AI drafts alreadyon it and clears their pending flags. Human drafts are left alone, and so are comments staff already
accepted.
The drafts are published one at a time with
update!, because each post's callbacks save its final text intothe feedback rating. The pending flags are cleared for all of them in a single
update_all, which also bumpsupdated_at, since read tracking depends on it (acts_as_readable on: :updated_at). That saves two queries perdraft. What remains, about 17 queries per draft, comes from the posts' own save callbacks.
Together these cover comments generated both before and after finalising, without a race between them.
Decisions made while building it:
whole class at once, so
ApplyEvaluationsJobpassesforce_draft: true.nobody is asked to rate an auto-published comment. The generated text is still saved as
original_feedback, so the rating record exists if a way to rate published comments is added later.2. Published comments appear without a page refresh
After grading finishes, the client reloads the answer and looks for the AI comment in the response. Two things
kept a published comment from arriving:
includes the latest AI comment the viewer is allowed to see: staff see any state, students only published
comments. This follows the same rule the full page load uses. The logic is in a shared
_ai_generated_comment.json.jbuilderpartial.AUTOGRADE_RUBRIC_SUCCESSwas only dispatched when the response contained a category breakdown,which students don't receive until their submission is published. It now fires when either the breakdown or
the comment is present, and both are optional.
gradingResultskeeps the existing breakdown when an actioncarries none.
Rubric-graded forum-post answers had the same problem, because their response never included the comment at
all. They now use the shared partial too.
3. Step-by-step assessments: a rubric question is passed once it has been submitted
In step-by-step assessments, the Continue and Finalise buttons, and the step a student can reach on page load
(
maxStep), depend on whether each answer is correct. Rubric grading has no notion of a wrong answer, whichcaused two problems:
the result that the Continue button checks. With "show MCQ answer" also on, Continue was permanently
disabled for rubric questions.
student could not move past that step.
The new rule is that a rubric-graded question is passed once the student has submitted it at least once,
however long grading takes and whether or not it succeeds.
maxStep):QuestionsConcern#correctly_answered_question_idsalso counts questions withgrading_mode: rubricthat have a submitted answer. This is a plain query on the question's grading mode.explanation, from a new_rubric_explanation.json.jbuilderpartial. Its
correctistrueonce the answer has been submitted and null before that. It carries nograde, so it no longer needs the check on whether the viewer may see grades. The client code is unchanged.
4. Removing the
autogradable: falseplaceholderThe RBR question had been hardcoded to
autogradable: false, as a placeholder until it was decided when AIfeedback should be auto-published. This PR settles that, so the question now reports its real
auto_gradable?. Two client components relied on the placeholder:!question.autogradable, a condition copied from TextResponse,where an auto-gradable question shows a different input instead. RBR has no other input, so with the real
value the box would have disappeared. The condition is removed.
gradingModefield on the question, sent for everyquestion type. Rubric questions always show "Answer submitted", and other types behave as before. This
also fixes rubric-graded forum-post answers, which already reported their real
autogradableand so wereshowing a green "Correct" banner.
5. Comment state and comment UI converted to TypeScript
The two reducers that store the AI comment, and the question comment thread that displays it, are converted to
TypeScript. The work is split into three commits so git history stays connected:
c586eeceddonly renames the five files (100% renames; they still contain the old JS at that commit).f43a775135converts thepostsandtopicsreducers.73d2caa272convertscommentForms,CommentsandCommentCard.Reducers.
posts,topicsandcommentFormsare nowcreateSlicereducers. They react to the bundle'sexisting legacy action types by matching on the type, through a shared
isOneOfhelper inassessment/utils/matchers.ts.gradingResults.tsnow uses the same helper too, which removes its four inlinetype checks and the
Action/UnknownActiontypes that existed only to support them.Topicintypes.tsnow declares the full payload the server sends (
id,questionId,submissionQuestionId,postIds), where itpreviously declared only
postIds.Each slice keeps the old state shape and reacts to exactly the same actions. The old and new action lists were
compared for
postsandtopics. ForcommentForms, the old and new reducers were run through the same 25actions, covering every action type it handles, and the state matched after each one. The deliberate
differences are:
have thrown.
commentFormsnow starts withisUpdatingCommentandannotationsdefined, where they were previouslymissing until the first relevant action. Its submitting flags are always booleans. It no longer has
annotationsDelayedComment, which nothing read or wrote.Components.
Commentsis a function component using typed selectors instead ofconnectand PropTypes. It reads the postids and the posts map separately and builds the list during render. Returning a newly built array from a
selector would re-render on every store update, and react-redux warns about that in development.
CommentCardis a function component typed againstCommentPostMiniEntity.publishCommentis now optional:Annotations.jsxnever passes it, so the old card would have thrown if an annotation had been a draft.AiFeedbackCommentCard's hand-written post type is now aPickof the shared type.CommentCardalso replaces the deprecatedConfirmationDialogwithPrompt, usingDeleteButton'sconfirmMessageprop, which several other delete buttons in the app already use. The delete isn't awaited, sothe prompt still closes when you confirm and can't be confirmed twice while the request is running.
manually_graded_spec.rbnow clicksPrompt's confirm button.The DOM hooks the feature specs use are unchanged:
#topic_<id>,#edit_post_<id>,#post_<id>, and theedit-comment/delete-commentclasses.Annotations.jsxandReadOnlyEditor.jsx, which read the samestate and render
CommentCard, needed no changes.6. AI feedback ratings are no longer sent to students
The comment payload sent
generatedRatingfor every AI comment, with no check on who was viewing it. Thatobject holds the AI's original text, the staff-edited text and the staff member's score, so when staff edited a
draft before publishing it, the student's browser received the AI's original wording and the rating. Forum
posts had the same problem with RagWise answers, where the rating also carries the answer's faithfulness and
relevance scores.
Both views (
_post.json.jbuilderand the forum's_post_list_data.json.jbuilder) now send the rating onlywhen
can?(:update, rating), the same check the rating endpoints use. That is granted to course staff only, soexactly the people who can rate a comment receive its rating. No client changes were needed: every component
reads
generatedRatingonly on drafts, which students never receive.Locales
en,koandzh.CodaveriSettings.codaveriSystemPromptDescriptionwas shortened intranslations.tsin06e3f2778c(2025-09-28), but the locale files were never updated. Thekoandzhversions still had the old paragraph with
{br}placeholders, which the form never passes, so Korean andChinese users were probably seeing a broken or raw message there.
Tests
AiGeneratedPostService: one example per mode, includingforce_draftproducing a draft under apublish mode,
publish_on_finaliseproducing a draft while attempting and a published comment after, andoriginal_feedbackstill being saved withratingleft unset in the publish modes.alone, and
draftmode is unaffected. There is also a case for a comment staff accepted beforefinalising: if the student has since replied, the topic must still be pending after finalising. Without the
workflow_state: 'draft'filter, the release would clear that flag and hide the student's question fromstaff.
latest comment in any state; with partial submission allowed, a student gets
explanation.correct == trueonce they have submitted.
next_unanswered: a drafted rubric answer does not pass the step; a submitted one does, even thoughgrading never set
correct.draft, rejects unsupported values, and savesfor a course manager.
generatedRatingandnone of the AI's original wording, while staff get the rating. The same checks cover a rated RagWise answer
in a forum topic, for a student and for a teaching assistant.
posts.test.tsandtopics.test.ts(5 cases each: loading, create/update, the optional AIcomment, no comment leaving state unchanged, and delete), and
commentForms.test.ts(5 cases: boxes emptiedon load while in-progress edits are kept, a new comment's lifecycle, a new annotation on a file with none yet,
an edit saved then deleted, and grading emptying a file's annotation boxes).
manually_graded_spec.rbcovers the comment UI end to end. Its delete step was updated forPrompt. It was not run locally and will run on CI.Each new spec was checked against the code it guards: with that code removed or reverted, the spec fails on
the intended assertion.
Deployment notes
No migration. Every existing course reads as
draft, so nothing changes until a course opts into anothermode. Switching modes affects only comments created afterwards.
Known limitations
grading-failure handler resets the question's result to null. After a refresh the student can move on.
Follow-ups (not in this PR)
Course::Discussion::Postrunsmark_self_as_readtwice on every update. It's registered on bothafter_saveandafter_update. This affects every post save, and is most of the per-draft cost whenfinalising a submission publishes its drafts.
CommentPostMiniEntity.createdAtis typed asDate, but the API sends a string.Commentswraps it inString(...)where it builds the AI card's React key. Fixing the shared type affects the discussion bundle too.topicShapein the submission bundle'spropTypes.jsdeclaresposts, but the field ispostIds. It is still used for annotation topics (with the correct field, so this bug is only cosmetic), so it is best fixed along with the eventual migration of the annotations code to TypeScript.