{"record":{"id":"40df2aabee10814d","repo":"instructure/canvas-lms","slug":"cannot-find-criterion-with-id-criterion-id","errorCode":null,"errorMessage":"Cannot find criterion with id #{criterion_id}","messagePattern":"Cannot find criterion with id #(.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"app/services/rubric_llm_service.rb","lineNumber":116,"sourceCode":"  # @return [Array<Hash>] normalized criteria set\n  #\n  # Example of text extraction format fed to LLM (rubric_to_text):\n  #   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?","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/app/services/rubric_llm_service.rb#L98-L134","documentation":"RubricLlmService#regenerate_criteria_via_llm raises this when a criterion_id is passed in regenerate_options but no criterion with that id exists in the rubric's criteria list (target_criterion is nil). It is a guard so LLM regeneration never silently no-ops for an unknown criterion. The id lookup happens against the rubric's loaded criteria array.","triggerScenarios":"Calling regenerate_criteria_via_llm with regenerate_options[:criterion_id] set to an id that does not match any criterion on the rubric — e.g. a stale id from a previously saved rubric, an id from a different rubric, or an id after criteria were regenerated/replaced.","commonSituations":"Frontend caches criterion ids from an older rubric version; user regenerates criteria then re-submits an old regenerate request; cross-rubric id reuse in API integrations; typos in id passed from a controller.","solutions":["Verify the criterion_id belongs to the current rubric's criteria before calling regenerate_criteria_via_llm.","Re-fetch the rubric (and its fresh criteria ids) before issuing a regeneration request after any save/regeneration.","If the criterion should exist, check that the rubric record passed to RubricLlmService.new is the up-to-date persisted rubric.","Handle the raised string error in the controller and return a 404-style response so the client refreshes its rubric state."],"exampleFix":"// before\ncriterion_id = params[:criterion_id] # stale id from an old render\nRubricLlmService.new(rubric).regenerate_criteria_via_llm(..., { criterion_id: criterion_id })\n// after\ncriterion = rubric.criteria.find { |c| c[:id].to_s == params[:criterion_id].to_s }\nreturn render json: { error: 'criterion not found' }, status: :not_found unless criterion\nRubricLlmService.new(rubric).regenerate_criteria_via_llm(..., { criterion_id: criterion[:id] })","handlingStrategy":"validation","validationCode":"criterion = rubric.criteria.find { |c| c[:id].to_s == criterion_id.to_s }\nraise ArgumentError, \"criterion #{criterion_id} not on rubric\" unless criterion","typeGuard":"def criterion_on_rubric?(rubric, id)\n  rubric.criteria.any? { |c| c[:id].to_s == id.to_s }\nend","tryCatchPattern":"begin\n  service.regenerate_criteria_via_llm(...)\nrescue RuntimeError => e\n  if e.message.start_with?('Cannot find criterion with id')\n    refresh_rubric_state! # refetch rubric and criteria ids\n  else\n    raise\n  end\nend","preventionTips":["Always re-fetch fresh criterion ids from the server after any rubric save or regeneration.","Never cache criterion ids across rubric versions in the client.","Validate criterion_id against rubric.criteria before invoking the service."],"tags":["rubric","llm","criteria","record-not-found"],"backgroundTag":"record-not-found","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"}