{"record":{"id":"cf7785194fff940f","repo":"we-promise/sure","slug":"label-must-be-a-non-negative-number","errorCode":null,"errorMessage":"#{label} must be a non-negative number.","messagePattern":"#(.+?) must be a non-negative number\\.","errorType":"exception","errorClass":"Assistant::Error","httpStatus":null,"severity":"warning","filePath":"app/models/assistant/function/update_budget.rb","lineNumber":150,"sourceCode":"      totals: {\n        budgeted_spending: format_money(budget.budgeted_spending),\n        expected_income: format_money(budget.expected_income),\n        allocated_spending: format_money(budget.allocated_spending),\n        available_to_allocate: format_money(budget.available_to_allocate)\n      },\n      updated_categories: updated,\n      message: \"Budget for #{budget.start_date.strftime('%B %Y')} updated.\"\n    }\n  rescue Assistant::Error => e\n    error(\"invalid_params\", e.message)\n  rescue ActiveRecord::RecordInvalid => e\n    error(\"validation_failed\", e.record.errors.full_messages.join(\"; \"))\n  end\n\n  private\n    def parse_amount!(raw, label)\n      value = Float(raw)\n      raise Assistant::Error, \"#{label} must be a non-negative number.\" if !value.finite? || value.negative?\n      value\n    rescue ArgumentError, TypeError\n      raise Assistant::Error, \"#{label} must be a non-negative number.\"\n    end\n\n    def find_budget_category!(budget, ref)\n      ref = ref.to_s.strip\n      raise Assistant::Error, \"Each categories entry needs a category name or id.\" if ref.blank?\n\n      category = valid_uuid?(ref) ? family.categories.find_by(id: ref) : nil\n      category ||= family.categories.where(\"LOWER(name) = ?\", ref.downcase).first\n\n      if category.nil?\n        if Category.all_uncategorized_names.any? { |name| name.casecmp?(ref) }\n          raise Assistant::Error, \"'#{ref}' is the unallocated remainder of budgeted_spending and cannot be set directly. Adjust budgeted_spending or category amounts instead.\"\n        end\n        raise Assistant::Error, \"Category '#{ref}' not found. Use get_categories to list categories.\"\n      end","sourceCodeStart":132,"sourceCodeEnd":168,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/assistant/function/update_budget.rb#L132-L168","documentation":"Raised by Assistant::Function::UpdateBudget#parse_amount! when Float(raw) succeeded but the value is not usable: !value.finite? (the string \"Infinity\" or \"NaN\" — Float() happily parses both) or value.negative?. The interpolated label names the offending field: \"budgeted_spending\", \"expected_income\", or \"amount for '<category>'\". Rescued by call into { success: false, error: \"invalid_params\" }.","triggerScenarios":"update_budget with budgeted_spending: -500 or expected_income: \"-0.01\" hits the negative branch at app/models/assistant/function/update_budget.rb:150; a categories entry with amount: \"NaN\" or \"Infinity\" (LLM-serialized non-finite numbers) hits the !finite? branch. Note the params schema declares minimum: 0 but strict_mode? is false, so enforcement falls to this method.","commonSituations":"LLM treating a refund/credit as a negative budget amount; arithmetic in the model producing -0.0 or NaN before calling the tool; JSON payload carrying string \"Infinity\" which JSON.parse itself would reject but tool-call argument parsing may pass through.","solutions":["Pass plain non-negative numbers (0 is allowed) — for refunds/credits use the transaction side, not budget amounts.","Clamp or correct the source value before calling: amount = [amount.to_f, 0.0].max when a floor of zero is the intended behavior.","If the LLM keeps producing negatives, reinforce the tool description ('Amounts are plain non-negative numbers') or the system prompt — the schema's minimum: 0 is not strictly enforced.","For NaN/Infinity, fix the upstream computation that produced a non-finite value instead of stringifying it."],"exampleFix":"# before\nupdate_budget.call({ \"budgeted_spending\" => -6500.0 })\n\n# after\nupdate_budget.call({ \"budgeted_spending\" => 6500.0 })\n# or, when flooring is intended:\nupdate_budget.call({ \"budgeted_spending\" => [raw_value.to_f, 0.0].max })","handlingStrategy":"validation","validationCode":"def valid_budget_amount?(raw)\n  value = Float(raw) rescue nil\n  value.is_a?(Float) && value.finite? && !value.negative?\nend","typeGuard":"# Ruby\ndef non_negative_finite?(raw)\n  v = Float(raw) rescue nil # handles strings and numerics; nil/Hash -> nil\n  !v.nil? && v.finite? && v >= 0\nend","tryCatchPattern":null,"preventionTips":["Send amounts as plain JSON numbers ≥ 0; encode credits/refunds elsewhere, never as negative budget amounts.","Floor computed values with [value, 0.0].max only when zero-flooring is actually intended business behavior.","Remember the schema's minimum: 0 is descriptive (strict_mode? false) — enforce non-negativity at the call site."],"tags":["validation","numeric","budget","assistant","tool-call"],"backgroundTag":"numeric-validation-failed","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}