{"record":{"id":"83caf046e3605461","repo":"instructure/canvas-lms","slug":"cannot-regenerate-criteria-with-learning-outcomes-attached","errorCode":null,"errorMessage":"Cannot regenerate criteria with learning outcomes attached","messagePattern":"Cannot regenerate criteria with learning outcomes attached","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/services/rubric_llm_service.rb","lineNumber":119,"sourceCode":"  #   criterion:c1:description=\"Clarity\"\n  #   rating:r1:description=\"Exemplary\"\n  def regenerate_criteria_via_llm(association_object, regenerate_options = {}, generate_options = {})\n    validate_rubric_and_association_object(association_object)\n\n    assignment = association_object\n    generate_options = resolve_regenerate_options(generate_options, regenerate_options)\n    incoming_criteria, existing_criteria_json, criteria_as_text, regenerable_criteria, learning_outcome_criteria_map, target_criterion =\n      normalize_incoming_criteria(regenerate_options)\n\n    criterion_id = regenerate_options[:criterion_id]\n\n    # Check if trying to regenerate a learning outcome criterion (not allowed)\n    if criterion_id.present?\n      if target_criterion.nil?\n        raise \"Cannot find criterion with id #{criterion_id}\"\n      end\n      if target_criterion[:learning_outcome_id].present?\n        raise \"Cannot regenerate criteria with learning outcomes attached\"\n      end\n    end\n\n    # If all criteria have learning outcomes, there's nothing to regenerate\n    # Return the original criteria with recalculated points\n    # Preserve all fields: learning_outcome_id, ignore_for_scoring, mastery_points, generated, etc.\n    if regenerable_criteria.empty?\n      total_points = generate_options[:total_points].to_f\n      points_per_criterion = calculate_points_per_criterion(total_points, incoming_criteria.size)\n\n      return incoming_criteria.each_with_index.map do |criterion, index|\n        criterion.dup.tap do |c|\n          c[:points] = points_per_criterion[index]\n          # Normalize ratings from hash to array format for frontend compatibility\n          c[:ratings] = normalize_ratings_array(c[:ratings]) if c[:ratings].present?\n        end\n      end\n    end","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/services/rubric_llm_service.rb#L101-L137","documentation":"RubricLlmService#regenerate_criteria_via_llm refuses to regenerate a single criterion that is tied to a learning outcome (criterion[:learning_outcome_id] present). Outcome criteria are managed by the outcomes system, so LLM rewriting them would desync the rubric from the outcome. The caller must exclude such criteria from regeneration.","triggerScenarios":"Calling regenerate_criteria_via_llm with regenerate_options[:criterion_id] pointing at a criterion whose learning_outcome_id is set — i.e. an outcome-aligned rubric row selected for regeneration in the rubric editor.","commonSituations":"User clicks a regenerate control on a criterion that Canvas linked to a learning outcome; API client lists all criteria ids including outcome-linked ones; UI failed to disable regenerate for outcome rows.","solutions":["Filter out criteria with learning_outcome_id present before offering/sending regeneration requests.","In the UI, hide or disable the regenerate action for outcome-linked criteria.","If regeneration of outcome criteria is truly needed, regenerate a non-outcome criterion or detach the outcome first (with user awareness of consequences)."],"exampleFix":"// before\nservice.regenerate_criteria_via_llm(..., { criterion_id: outcome_linked_criterion[:id] })\n// after\nif outcome_linked_criterion[:learning_outcome_id].present?\n  return render json: { error: 'cannot regenerate outcome criterion' }, status: :unprocessable_entity\nend\nservice.regenerate_criteria_via_llm(..., { criterion_id: outcome_linked_criterion[:id] })","handlingStrategy":"validation","validationCode":"criterion = rubric.criteria.find { |c| c[:id].to_s == criterion_id.to_s }\nreturn :skip if criterion && criterion[:learning_outcome_id].present?","typeGuard":"def outcome_linked?(criterion)\n  criterion.is_a?(Hash) && criterion[:learning_outcome_id].present?\nend","tryCatchPattern":"begin\n  service.regenerate_criteria_via_llm(...)\nrescue RuntimeError => e\n  raise unless e.message == 'Cannot regenerate criteria with learning outcomes attached'\n  notify_user('outcome-linked criteria cannot be regenerated')\nend","preventionTips":["Disable regenerate controls for criteria whose learning_outcome_id is set in the UI.","Filter client-side criteria lists to exclude outcome rows from bulk regenerate payloads."],"tags":["rubric","llm","learning-outcomes","unsupported-operation"],"backgroundTag":"unsupported-operation","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}