{"record":{"id":"93a161abc96f6cc6","repo":"gastownhall/beads","slug":"errvalidation","errorCode":"ErrValidation","errorMessage":"%w: update requires actor and issue ID","messagePattern":"%w: update requires actor and issue ID","errorType":"validation","errorClass":"storage.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/execution.go","lineNumber":135,"sourceCode":"// skippedDependencyError refuses a guarded create whose requested edges were\n// not all written. The batch engine drops a dangling edge so a partial import\n// still lands, but a guarded create that reported success while silently\n// discarding a parent, waits-for, or explicit dependency is data loss: the\n// caller has no way to learn the relationship is missing. Refusing rolls the\n// whole create back with the enclosing transaction.\nfunc skippedDependencyError(skipped []skippedDependency) error {\n\tedges := make([]string, 0, len(skipped))\n\tfor _, edge := range skipped {\n\t\tedges = append(edges, fmt.Sprintf(\"%s -> %s (%s)\", edge.issueID, edge.dependsOnID, edge.reason))\n\t}\n\treturn fmt.Errorf(\"create: dependencies could not be created: %s: %w\", strings.Join(edges, \"; \"), storage.ErrNotFound)\n}\n\n// ExecuteUpdate applies a guarded update in tx and reports durable tables changed.\nfunc ExecuteUpdate(ctx context.Context, tx *sql.Tx, request publicops.UpdateRequest) (publicops.UpdateResult, ChangedTables, error) {\n\tattempt := CloneUpdateRequest(request)\n\tif attempt.Actor == \"\" || attempt.IssueID == \"\" {\n\t\treturn publicops.UpdateResult{}, nil, fmt.Errorf(\"%w: update requires actor and issue ID\", storage.ErrValidation)\n\t}\n\tif err := ValidateUpdateRequest(attempt); err != nil {\n\t\treturn publicops.UpdateResult{}, nil, err\n\t}\n\tif err := ValidateMetadataPatch(attempt.Patch.Metadata); err != nil {\n\t\treturn publicops.UpdateResult{}, nil, err\n\t}\n\t// The plane restriction is resolved HERE, inside the update's own\n\t// transaction, so a caller that serves durable issues only cannot be handed\n\t// a wisp by a resolve that ran earlier.\n\tif attempt.IssuePlaneOnly && IsActiveWispInTx(ctx, tx, attempt.IssueID) {\n\t\treturn publicops.UpdateResult{}, nil, fmt.Errorf(\"%w: issue %s\", storage.ErrNotFound, attempt.IssueID)\n\t}\n\ttables := ChangedTables{}\n\tbefore, err := GetIssueInTx(ctx, tx, attempt.IssueID)\n\tif err != nil {\n\t\treturn publicops.UpdateResult{}, nil, err\n\t}","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/execution.go#L117-L153","documentation":"ExecuteUpdate applies a guarded issue update inside a transaction and requires an explicit Actor and IssueID on the request. When either is empty it returns storage.ErrValidation wrapped with this message, before any validation or database work runs. This forces audit-attributable, explicitly targeted updates.","triggerScenarios":"Calling ExecuteUpdate (directly or via applyUpdate / spliceMetadataRefs) with a publicops.UpdateRequest where Actor == \"\" or IssueID == \"\".","commonSituations":"Programmatic updates that set Patch fields but forget Actor; constructing UpdateRequest from CLI flags where the actor flag was omitted; ID left empty when the caller expected auto-detection from context.","solutions":["Set Actor on the UpdateRequest (e.g. the CLI username or service identity) before calling.","Set IssueID to the target issue's ID (bd-xxxx).","If building requests from flags, default Actor from config/env at the CLI boundary.","Run storage.ValidateUpdateRequest-style checks client-side before invoking."],"exampleFix":"// before\nreq := publicops.UpdateRequest{IssueID: \"bd-42\", Patch: patch}\n// after\nreq := publicops.UpdateRequest{IssueID: \"bd-42\", Actor: \"agent\", Patch: patch}","handlingStrategy":"validation","validationCode":"func validUpdate(r publicops.UpdateRequest) error {\n\tif r.Actor == \"\" {\n\t\treturn fmt.Errorf(\"update requires actor\")\n\t}\n\tif r.IssueID == \"\" {\n\t\treturn fmt.Errorf(\"update requires issue ID\")\n\t}\n\treturn nil\n}","typeGuard":null,"tryCatchPattern":"res, _, err := ExecuteUpdate(ctx, tx, req)\nif errors.Is(err, storage.ErrValidation) {\n\treturn fmt.Errorf(\"request rejected: %w (set Actor and IssueID)\", err)\n}","preventionTips":["Build requests through a constructor that requires actor and issueID parameters.","Default Actor from config/env at the CLI boundary so it is never empty.","Validate requests with storage validation helpers before opening a transaction."],"tags":["validation","update","actor","api-misuse"],"backgroundTag":"missing-required-field","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}