{"record":{"id":"50b8a3013f8efbce","repo":"gastownhall/beads","slug":"w-update-estimated-minutes-w","errorCode":null,"errorMessage":"%w: update estimated_minutes: %w","messagePattern":"%w: update estimated_minutes: %w","errorType":"validation","errorClass":"storage.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/aggregate.go","lineNumber":92,"sourceCode":"\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() {\n\t\treturn fmt.Errorf(\"%w: invalid persistence mode %q\", storage.ErrValidation, patch.Persistence.Value)\n\t}\n\treturn nil\n}\n\n// ValidateMetadataPatch checks mutually exclusive metadata edits.\nfunc ValidateMetadataPatch(patch publicops.MetadataPatch) error {\n\tif patch.Replace.Set && (patch.Merge.Set || len(patch.Set) > 0 || len(patch.Unset) > 0) {\n\t\treturn fmt.Errorf(\"%w: cannot combine metadata replacement with incremental metadata edits\", storage.ErrValidation)\n\t}\n\treturn nil\n}\n\n// ValidateScalarUpdates checks typed scalar values before they reach SQL.\nfunc ValidateScalarUpdates(ctx context.Context, tx DBTX, updates map[string]interface{}) error {","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/aggregate.go#L74-L110","documentation":"This error wraps types.ValidateIssueEstimatedMinutes failures when a patch sets EstimatedMinutes. The aggregate validator rejects invalid estimates (e.g. negative or over-limit values) before the row is updated, and the error chain carries both storage.ErrValidation and the underlying cause.","triggerScenarios":"ExecuteUpdate with Patch.EstimatedMinutes.Set=true and a value failing types.ValidateIssueEstimatedMinutes — typically a negative duration or a value above the allowed ceiling.","commonSituations":"Import scripts writing estimated durations from spreadsheets with negative or placeholder values (-1, 999999); UIs letting users type minutes freely without bounds; unit confusion (hours entered where minutes are expected, producing huge numbers).","solutions":["Clamp/reject estimates before patching: require 0 <= minutes <= the accepted maximum","Parse durations into minutes explicitly (hours*60) instead of passing raw units","Check the wrapped inner error to confirm which bound was violated"],"exampleFix":"// before\npatch.EstimatedMinutes = publicops.SetField[int]{Set: true, Value: mins} // mins could be -5\n// after\nif mins < 0 || mins > maxEstimatedMinutes { return fmt.Errorf(\"estimate out of range: %d\", mins) }\npatch.EstimatedMinutes = publicops.SetField[int]{Set: true, Value: mins}","handlingStrategy":"validation","validationCode":"if patch.EstimatedMinutes.Set {\n  if err := types.ValidateIssueEstimatedMinutes(patch.EstimatedMinutes.Value); err != nil {\n    return fmt.Errorf(\"pre-check estimate: %w\", err)\n  }\n}","typeGuard":"func estimateOK(m int) bool { return types.ValidateIssueEstimatedMinutes(m) == nil }","tryCatchPattern":"if err := issueops.ExecuteUpdate(ctx, tx, req); errors.Is(err, storage.ErrValidation) {\n  // handle estimate out-of-range: clamp or prompt user\n}","preventionTips":["Clamp estimates to [0, max] at the input layer","Convert hours/days to minutes explicitly before patching","Sanitize imported data (skip or fix negative/placeholder estimates) before bulk updates"],"tags":["go","validation","estimated-minutes","update","storage"],"backgroundTag":"field-validation-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}