{"record":{"id":"b40a5d7b194a1f72","repo":"instructure/canvas-lms","slug":"points-for-tier-index-must-be-less-than-points-for-prior","errorCode":null,"errorMessage":"Points for tier %{index} must be less than points for prior tier (%{points} is greater than %{prior})","messagePattern":"Points for tier %(.+?) must be less than points for prior tier \\(%(.+?) is greater than %(.+?)\\)","errorType":"validation","errorClass":"InvalidDataError","httpStatus":null,"severity":"error","filePath":"lib/outcomes/csv_importer.rb","lineNumber":179,"sourceCode":"      if object[:mastery_points].present?\n        object[:mastery_points] = strict_parse_float(object[:mastery_points], I18n.t(\"mastery points\"))\n      end\n      if object[:friendly_description].present? && object[:friendly_description].length > 255\n        raise InvalidDataError, I18n.t(\"Friendly description is too long (maximum is 255 characters)\")\n      end\n\n      import_object(object)\n    end\n\n    def parse_ratings(ratings)\n      prior = nil\n      drop_trailing_nils(ratings).each_slice(2).to_a.map.with_index(1) do |(points, description), index|\n        raise InvalidDataError, I18n.t(\"Points for rating tier %{index} not present\", index:) if points.nil? || points.blank?\n\n        points = strict_parse_float(points, I18n.t(\"rating tier %{index} threshold\", index:))\n\n        if prior.present? && prior < points\n          raise InvalidDataError, I18n.t(\n            \"Points for tier %{index} must be less than points for prior tier (%{points} is greater than %{prior})\",\n            index:,\n            prior:,\n            points:\n          )\n        end\n\n        prior = points\n        { points:, description: }\n      end\n    end\n\n    def normalize_i18n(string)\n      raise ArgumentError if string.blank?\n\n      separator = I18n.t(\"number.format.separator\")\n      delimiter = I18n.t(\"number.format.delimiter\")\n      string.gsub(delimiter, \"\").gsub(separator, \".\")","sourceCodeStart":161,"sourceCodeEnd":197,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/lib/outcomes/csv_importer.rb#L161-L197","documentation":"Raised by Outcomes::CsvImporter#parse_ratings when a tier's points value is greater than the previous tier's points. Outcome ratings must be in descending point order (first tier is highest); each parsed points value must be <= the prior one or an InvalidDataError reports both values. This preserves Canvas's expectation that mastery ratings go from highest to lowest.","triggerScenarios":"Ratings written in ascending order, e.g. `3,Meets,5,Exemplary` — at tier 2, points 5 > prior 3; duplicated or shuffled point values; users listing tiers from lowest to highest mastery.","commonSituations":"Authors thinking of ratings as ascending rubric levels; reordering tiers in a spreadsheet without re-sorting points; converting from tools that use ascending scales.","solutions":["Reverse the rating order so points descend: highest tier (e.g. Exemplary) first.","Re-sort the ratings pairs by points descending before importing.","Verify each tier's points is less than or equal to the previous tier's points."],"exampleFix":"// before\nratings: 1,Beginning,3,Meets,5,Exemplary\n// after\nratings: 5,Exemplary,3,Meets,1,Beginning","handlingStrategy":"validation","validationCode":"points = cells.each_slice(2).map(&:first).compact.map(&:to_f)\nraise 'rating points must descend' unless points.each_cons(2).all? { |a, b| a >= b }","typeGuard":null,"tryCatchPattern":"begin\n  importer.run\nrescue Outcomes::Import::InvalidDataError => e\n  errors << \"Row #{i}: #{e.message}\"\nend","preventionTips":["List rating tiers highest-points first (Exemplary before Meets)","Sort ratings pairs by points descending in preprocessing","Visual check: points column should read top-to-bottom as non-increasing"],"tags":["csv","ratings","ordering","import"],"backgroundTag":"value-out-of-range","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"}