{"record":{"id":"f727a7ec6133d322","repo":"we-promise/sure","slug":"ref-is-the-unallocated-remainder-of-budgeted","errorCode":null,"errorMessage":"'#{ref}' is the unallocated remainder of budgeted_spending and cannot be set directly. Adjust budgeted_spending or category amounts instead.","messagePattern":"'#(.+?)' is the unallocated remainder of budgeted_spending and cannot be set directly\\. Adjust budgeted_spending or category amounts instead\\.","errorType":"exception","errorClass":"Assistant::Error","httpStatus":null,"severity":"warning","filePath":"app/models/assistant/function/update_budget.rb","lineNumber":165,"sourceCode":"  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\n\n    def error(key, message)\n      { success: false, error: key, message: message }\n    end\nend\n","sourceCodeStart":147,"sourceCodeEnd":182,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/assistant/function/update_budget.rb#L147-L182","documentation":"Raised by find_budget_category! when the requested category reference case-insensitively matches one of Category.all_uncategorized_names. \"Uncategorized\" is not a stored category: it is the derived remainder of budget.budgeted_spending minus the sum of per-category allocations, so writing it directly is refused to keep the invariant that it equals the remainder.","triggerScenarios":"update_budget with categories: [{ \"category\" => \"Uncategorized\", \"amount\" => 500 }] (any case — the check uses casecmp?) reaches app/models/assistant/function/update_budget.rb:164-166 because no real category matched the name, but a reserved uncategorized name did.","commonSituations":"LLM or user trying to earmark the leftover bucket; families whose budgeted_spending exceeds sum of category amounts so the model 'helpfully' allocates the difference to Uncategorized; localized uncategorized names being matched through all_uncategorized_names.","solutions":["To shrink the leftover, lower budgeted_spending: update_budget({ month: ..., budgeted_spending: <smaller total> }).","To allocate the leftover, raise amounts on real categories (or create them first) — their sum then consumes the remainder.","Tell the model up front (the tool description already states it) that the Uncategorized bucket is derived and not directly settable."],"exampleFix":"# before\nupdate_budget.call({ \"categories\" => [{ \"category\" => \"Uncategorized\", \"amount\" => 500 }] })\n\n# after — grow a real category and/or shrink the total instead\nupdate_budget.call({\n  \"budgeted_spending\" => 6000,\n  \"categories\" => [{ \"category\" => \"Groceries\", \"amount\" => 900 }]\n})","handlingStrategy":"validation","validationCode":"reserved = Category.all_uncategorized_names.map(&:downcase)\nif reserved.include?(ref.to_s.downcase)\n  raise ArgumentError, \"uncategorized is derived — adjust budgeted_spending or real categories\"\nend","typeGuard":"# Ruby\ndef uncategorized_ref?(ref)\n  Category.all_uncategorized_names.any? { |name| name.casecmp?(ref.to_s) }\nend","tryCatchPattern":null,"preventionTips":["Route 'allocate the leftover' requests to budgeted_spending or real categories, never to the Uncategorized bucket.","Teach the model the invariant early: the tool description already states the bucket is the unallocated remainder of budgeted_spending.","Check available_to_allocate from get_budget output to decide whether to raise category sums or lower the total."],"tags":["budget","business-rule","assistant","tool-call","validation"],"backgroundTag":"invalid-parameters","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}