{"record":{"id":"2f332110e1e7a103","repo":"we-promise/sure","slug":"reliability-grade-must-be-one-of-grades-join","errorCode":null,"errorMessage":"reliability grade must be one of #{GRADES.join(\", \")} (got #{suffix[:grade].inspect})","messagePattern":"reliability grade must be one of #(.+?) \\(got #(.+?)\\)","errorType":"exception","errorClass":"Provenance::Citation::InvalidError","httpStatus":null,"severity":"error","filePath":"app/models/provenance/citation.rb","lineNumber":51,"sourceCode":"    class InvalidError < StandardError; end\n\n    attr_reader :raw, :text, :grade\n\n    class << self\n      # Returns a Citation, or raises InvalidError with a message written for the\n      # caller that has to fix it (an agent, usually).\n      def parse!(raw)\n        value = raw.to_s.strip\n\n        raise InvalidError, \"source citation is required\" if value.blank?\n        raise InvalidError, \"source citation must be #{MAX_LENGTH} characters or fewer\" if value.length > MAX_LENGTH\n\n        if value.match?(ESTIMATED_MARKER) && !value.start_with?(ESTIMATED_PREFIX)\n          raise InvalidError, \"estimated values must start with the exact prefix #{ESTIMATED_PREFIX.inspect}\"\n        end\n\n        if (suffix = value.match(GRADE_SUFFIX)) && !GRADES.include?(suffix[:grade])\n          raise InvalidError, \"reliability grade must be one of #{GRADES.join(\", \")} (got #{suffix[:grade].inspect})\"\n        end\n\n        match = value.match(FORMAT)\n        raise InvalidError, \"source citation does not match #{grammar}\" unless match\n\n        text = match[:text].to_s.strip\n        if text.length < MIN_TEXT_LENGTH\n          raise InvalidError, \"source citation must name the document it came from\"\n        end\n\n        estimated = match[:estimated].present?\n        grade = match[:grade]\n\n        if estimated && grade.blank?\n          raise InvalidError, \"estimated values must carry a reliability grade, e.g. \\\"#{ESTIMATED_PREFIX}... (grade: C)\\\"\"\n        end\n\n        new(raw: value, text: text, estimated: estimated, grade: grade)","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provenance/citation.rb#L33-L69","documentation":"Provenance::Citation.parse! raises InvalidError when the string ends with a '(grade: X)' suffix (matched by GRADE_SUFFIX) whose grade is not one of A, B, C. The suffix is matched separately from the main FORMAT regex so an invalid grade like '(grade: D)' fails loudly instead of folding into the citation text and silently producing an ungraded source.","triggerScenarios":"Citations ending in '(grade: D)', '(grade: a)' (lowercase, not in the A-C set as matched — FORMAT only captures [ABC] uppercase), '(grade: A+)', or '(grade: )' with an empty value.","commonSituations":"Agent inventing a wider grade scale; user typing lowercase or numeric grades; typo in the suffix.","solutions":["Change the grade to A, B, or C (uppercase) — e.g. 'estimated: proxy from peer ETF (grade: C)'.","If the grade is uncertain, C is the designated 'proxy/assumption' grade.","Strip the suffix entirely if the value is not graded — but estimates then need a valid grade, so keep A-C for estimated:.","Validate with Provenance::Citation.valid? before persisting and surface the allowed set in the UI."],"exampleFix":"# before\nProvenance::Citation.parse!(\"Bank statement p.3 (grade: D)\")\n\n# after\nProvenance::Citation.parse!(\"Bank statement p.3 (grade: C)\")","handlingStrategy":"validation","validationCode":"suffix = value.to_s.match(/\\(grade:\\s*([^)]*)\\)\\s*\\z/)\nsuffix.nil? || %w[A B C].include?(suffix[1])","typeGuard":"def grade_suffix_ok?(v)\n  m = v.to_s.match(/\\(grade:\\s*([^)]*)\\)\\s*\\z/)\n  m.nil? || Provenance::Citation::GRADES.include?(m[1])\nend","tryCatchPattern":"begin\n  Provenance::Citation.parse!(source)\nrescue Provenance::Citation::InvalidError => e\n  errors.add(:source, e.message) # message names allowed grades and the bad value\nend","preventionTips":["Constrain grade input to a select of A/B/C in the UI.","Tell agents only A, B, C exist; C is for proxies/assumptions.","Never let the suffix fail silently into the citation text — keep GRADE_SUFFIX and FORMAT in sync when editing."],"tags":["provenance","validation","enum","ruby"],"backgroundTag":"invalid-enum-value","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}