{"record":{"id":"8b20fa665584c808","repo":"we-promise/sure","slug":"each-categories-entry-needs-a-category-name-or-id","errorCode":null,"errorMessage":"Each categories entry needs a category name or id.","messagePattern":"Each categories entry needs a category name or id\\.","errorType":"exception","errorClass":"Assistant::Error","httpStatus":null,"severity":"warning","filePath":"app/models/assistant/function/update_budget.rb","lineNumber":158,"sourceCode":"    }\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\n\n      budget.budget_categories.find_by(category_id: category.id) ||\n        raise(Assistant::Error, \"No budget row exists for category '#{category.name}' in #{budget.to_param}.\")\n    end\n\n    def format_money(value)\n      Money.new(value || 0, family.currency).format\n    end","sourceCodeStart":140,"sourceCodeEnd":176,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/assistant/function/update_budget.rb#L140-L176","documentation":"Raised by Assistant::Function::UpdateBudget#find_budget_category! when a categories entry carries no usable category reference. ref comes from change[\"category\"] when the entry is a Hash, or nil when it is not a Hash at all (update_budget.rb:115); after to_s.strip it must be non-blank.","triggerScenarios":"update_budget called with categories: [{ \"amount\" => 100 }] (no \"category\" key), categories: [{ \"category\" => \"  \", \"amount\" => 100 }] (whitespace-only), or categories: [\"Groceries\"] (a plain string entry — not a Hash — so ref becomes nil). All reach the blank check at app/models/assistant/function/update_budget.rb:158.","commonSituations":"LLM emitting shorthand array entries instead of { category:, amount: } objects; schema-drift between what the model was told and what it produced; hand-written tool-call JSON missing the required property (the schema lists both keys required but strict_mode? is false).","solutions":["Shape every entry as { \"category\" => <name or id>, \"amount\" => <number> } — both keys present, category non-empty.","If you aggregate caller-side input, validate entries with an each-check before invoking the tool.","Give the LLM the exact object shape in the prompt or fix the tool description example if drift persists."],"exampleFix":"# before\nupdate_budget.call({ \"categories\" => [\"Groceries\"] })\n\n# after\nupdate_budget.call({ \"categories\" => [{ \"category\" => \"Groceries\", \"amount\" => 900 }] })","handlingStrategy":"validation","validationCode":"params[\"categories\"].to_a.each do |entry|\n  unless entry.is_a?(Hash) && entry[\"category\"].to_s.strip.present?\n    raise ArgumentError, \"each categories entry needs a non-empty category (name or id)\"\n  end\nend","typeGuard":"# Ruby\ndef valid_category_entry?(entry)\n  entry.is_a?(Hash) && entry[\"category\"].is_a?(String) && entry[\"category\"].strip.present? && !entry[\"amount\"].nil?\nend","tryCatchPattern":null,"preventionTips":["Always build entries as { \"category\" => name_or_id, \"amount\" => number } with both keys present.","Validate the array shape caller-side before invoking update_budget — one malformed entry aborts the whole transaction's work.","If the LLM emits bare strings or missing keys, fix the prompt/tool description example rather than catching after the fact."],"tags":["validation","budget","assistant","tool-call","missing-parameter"],"backgroundTag":"missing-required-field","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}