{"record":{"id":"155b564ba379a58c","repo":"gastownhall/beads","slug":"w-invalid-forced-assignee-transfer","errorCode":null,"errorMessage":"%w: invalid forced assignee transfer","messagePattern":"%w: invalid forced assignee transfer","errorType":"validation","errorClass":"storage.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/aggregate.go","lineNumber":77,"sourceCode":"\t\t{patch.DueAt.Set, \"due_at\", patch.DueAt.Value},\n\t\t{patch.DeferUntil.Set, \"defer_until\", patch.DeferUntil.Value},\n\t} {\n\t\tif field.set {\n\t\t\tupdates[field.key] = field.val\n\t\t}\n\t}\n\treturn updates\n}\n\n// ValidateUpdateRequest checks mutually exclusive guarded-update options and\n// the canonical field values every backend must reject identically. Backends\n// call it before touching the row so an invalid patch cannot half-apply.\nfunc ValidateUpdateRequest(request publicops.UpdateRequest) error {\n\tif request.Claim && (request.ExpectedAssignee != nil || request.ExpectedStatus != nil) {\n\t\treturn fmt.Errorf(\"%w: claim cannot use expected assignee or status\", storage.ErrValidation)\n\t}\n\tif request.ForceAssigneeTransfer && (request.Claim || !request.Patch.Assignee.Set || request.ExpectedAssignee != nil) {\n\t\treturn fmt.Errorf(\"%w: invalid forced assignee transfer\", storage.ErrValidation)\n\t}\n\tpatch := request.Patch\n\tif patch.Title.Set {\n\t\tif err := types.ValidateIssueTitle(patch.Title.Value); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: update title: %w\", storage.ErrValidation, err)\n\t\t}\n\t}\n\tif patch.Priority.Set {\n\t\tif err := types.ValidateIssuePriority(patch.Priority.Value); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: update priority: %w\", storage.ErrValidation, err)\n\t\t}\n\t}\n\tif patch.EstimatedMinutes.Set {\n\t\tif err := types.ValidateIssueEstimatedMinutes(patch.EstimatedMinutes.Value); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: update estimated_minutes: %w\", storage.ErrValidation, err)\n\t\t}\n\t}\n\tif patch.Persistence.Set && !patch.Persistence.Value.IsValid() {","sourceCodeStart":59,"sourceCodeEnd":95,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/aggregate.go#L59-L95","documentation":"ValidateUpdateRequest returns this when ForceAssigneeTransfer is enabled but its preconditions are violated: the request also sets Claim, the patch does not actually set an assignee, or ExpectedAssignee is present. Forced transfer is meant to be an unambiguous, standalone reassignment, so any of those combinations is rejected before the row is modified. The error wraps storage.ErrValidation.","triggerScenarios":"ExecuteUpdate with request.ForceAssigneeTransfer=true plus any of: request.Claim=true, request.Patch.Assignee.Set=false (no assignee supplied), or request.ExpectedAssignee != nil.","commonSituations":"Admin tooling that force-reassigns issues but builds the patch conditionally so Assignee ends up unset; copying a claim-style request and flipping on ForceAssigneeTransfer; a UI sending stale ExpectedAssignee fields alongside a force transfer.","solutions":["Ensure ForceAssigneeTransfer is combined with Patch.Assignee.Set=true and a target assignee value","Remove Claim and ExpectedAssignee from the request when doing a forced transfer","Send ForceAssigneeTransfer as the only guarded option on the request"],"exampleFix":"// before\nreq := publicops.UpdateRequest{ID: id, ForceAssigneeTransfer: true, Claim: true}\n// after\nreq := publicops.UpdateRequest{\n  ID: id,\n  ForceAssigneeTransfer: true,\n  Patch: publicops.UpdatePatch{Assignee: publicops.SetField[string]{Set: true, Value: newOwner}},\n}","handlingStrategy":"validation","validationCode":"func validForceTransfer(r publicops.UpdateRequest) bool {\n  return r.ForceAssigneeTransfer && !r.Claim && r.ExpectedAssignee == nil && r.Patch.Assignee.Set\n}\nif !validForceTransfer(req) { return errors.New(\"invalid forced assignee transfer request\") }","typeGuard":"func isForceTransferReady(r publicops.UpdateRequest) bool {\n  return r.ForceAssigneeTransfer && r.Patch.Assignee.Set && !r.Claim && r.ExpectedAssignee == nil\n}","tryCatchPattern":"if err := issueops.ValidateUpdateRequest(req); errors.Is(err, storage.ErrValidation) {\n  // fix request: ensure Assignee set, drop Claim/ExpectedAssignee\n}","preventionTips":["Always set Patch.Assignee.Set=true with a value when forcing a transfer","Keep forced transfers in a code path separate from claim logic","Run ValidateUpdateRequest in a preflight test for every guarded-option combination you ship"],"tags":["go","validation","storage","assignee","force-transfer"],"backgroundTag":"conflicting-update-options","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}