{"record":{"id":"ae6f4a25960f3c25","repo":"gastownhall/beads","slug":"w-add-dependencies-requires-at-least-one-edge","errorCode":null,"errorMessage":"%w: add dependencies requires at least one edge","messagePattern":"%w: add dependencies requires at least one edge","errorType":"validation","errorClass":"storage.ErrValidation","httpStatus":null,"severity":"warning","filePath":"internal/storage/issueops/dependency_editor.go","lineNumber":33,"sourceCode":"// DependencyEditor implementation shares. It lives here rather than in each of\n// them because a rule enforced on one backend and not the other is not a\n// contract.\n//\n// The self-dependency refusal is here, ahead of the per-edge cycle probe and\n// for EVERY edge type, for the reason the domain path states: a blocking\n// self-edge otherwise trips the cycle check and reports the wrong refusal, and\n// SkipPerEdgeCycleCheck would skip it entirely.\n//\n// The type check is that there IS a type — non-empty, within the column's\n// length. It is deliberately not a membership test: the vocabulary is an open,\n// workspace-configurable set (see the Dep* constants), so refusing an unlisted\n// type would refuse a workspace's own.\nfunc ValidateAddDependenciesRequest(request publicops.AddDependenciesRequest) error {\n\tif request.Actor == \"\" {\n\t\treturn fmt.Errorf(\"%w: add dependencies requires an actor\", storage.ErrValidation)\n\t}\n\tif len(request.Edges) == 0 {\n\t\treturn fmt.Errorf(\"%w: add dependencies requires at least one edge\", storage.ErrValidation)\n\t}\n\tfor i, edge := range request.Edges {\n\t\tif edge.IssueID == \"\" || edge.DependsOnID == \"\" {\n\t\t\treturn fmt.Errorf(\"%w: add dependencies edge %d requires both endpoints\", storage.ErrValidation, i)\n\t\t}\n\t\tif !edge.Type.IsValid() {\n\t\t\treturn fmt.Errorf(\"%w: add dependencies edge %d requires a dependency type (max %d chars)\",\n\t\t\t\tstorage.ErrValidation, i, types.MaxDependencyTypeLen)\n\t\t}\n\t\tif edge.IssueID == edge.DependsOnID {\n\t\t\treturn fmt.Errorf(\"%w: %s cannot depend on itself\", domain.ErrSelfDependency, edge.IssueID)\n\t\t}\n\t}\n\treturn nil\n}\n\n// ValidateRemoveDependencyRequest applies the request rules every\n// DependencyEditor implementation shares for a removal.","sourceCodeStart":15,"sourceCodeEnd":51,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/dependency_editor.go#L15-L51","documentation":"ValidateAddDependenciesRequest rejects requests whose Edges slice is empty, since a no-op dependency write would silently succeed otherwise. The guard is wrapped with storage.ErrValidation for errors.Is() matching. It's raised before touching the database.","triggerScenarios":"Calling AddDependencies with Edges == nil or len(Edges) == 0 — e.g. an upstream batch builder filtered out all edges but the caller still invoked the API.","commonSituations":"Batch pipelines where a filter/dedupe step emptied the edge list; CLI flag parsing that produced no edges; automation generating zero changes and calling the API unconditionally.","solutions":["Skip the call entirely when there are no edges (no-op is fine) instead of invoking AddDependencies.","Check that edge collection upstream actually produced edges (log the count before the call).","Match with errors.Is(err, storage.ErrValidation) to distinguish from storage errors."],"exampleFix":"// before\n_ = store.AddDependencies(ctx, req) // req.Edges may be empty\n// after\nif len(req.Edges) == 0 {\n    return nil // nothing to do\n}\n_ = store.AddDependencies(ctx, req)","handlingStrategy":"validation","validationCode":"if len(req.Edges) == 0 {\n    return nil // no-op: skip the call entirely\n}","typeGuard":"func hasEdges(req publicops.AddDependenciesRequest) bool { return len(req.Edges) > 0 }","tryCatchPattern":"err := store.AddDependencies(ctx, req)\nif errors.Is(err, storage.ErrValidation) && len(req.Edges) == 0 {\n    return nil // treat as intentional no-op\n}","preventionTips":["Guard call sites with a len(Edges) > 0 check.","Log edge counts in batch pipelines so silent emptying is visible.","Only call AddDependencies when there is actual work."],"tags":["validation","dependencies","empty-input"],"backgroundTag":"empty-batch-request","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}