{"record":{"id":"e21c2f56a990a5f7","repo":"gastownhall/beads","slug":"add-deps-d-issueid-and-dependsonid-must-be-non","errorCode":null,"errorMessage":"add deps[%d]: IssueID and DependsOnID must be non-empty","messagePattern":"add deps\\[(.+?)\\]: IssueID and DependsOnID must be non-empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/dependency.go","lineNumber":692,"sourceCode":"}\n\n// AddDependencies asserts every edge in one pass, writing each to the plane its\n// own source lives in. The ordering and the final cycle gate deliberately do\n// not partition by plane, because the hierarchy a blocking edge is checked\n// against and the graph the gate walks both span the two tables.\nfunc (u *dependencyUseCaseImpl) AddDependencies(ctx context.Context, deps []*types.Dependency, actor string, opts BulkAddDepsOpts) (BulkAddDepsResult, error) {\n\tif len(deps) == 0 {\n\t\treturn BulkAddDepsResult{Added: []*types.Dependency{}}, nil\n\t}\n\t// Validate the entire input shape before the first write. Multi-edge callers\n\t// run in a UOW, but this also avoids an avoidable partial prefix for direct\n\t// use-case consumers.\n\tfor i, dep := range deps {\n\t\tif dep == nil {\n\t\t\treturn BulkAddDepsResult{}, fmt.Errorf(\"add deps[%d]: dep must not be nil\", i)\n\t\t}\n\t\tif dep.IssueID == \"\" || dep.DependsOnID == \"\" {\n\t\t\treturn BulkAddDepsResult{}, fmt.Errorf(\"add deps[%d]: IssueID and DependsOnID must be non-empty\", i)\n\t\t}\n\t\t// Self-dependency guard mirrors the single-edge add() path and\n\t\t// issueops.CheckDependencyCycleInTx: reject a self-edge for ALL dep\n\t\t// types before the hierarchy/cycle probe, so a scheduling self-edge is\n\t\t// typed as ErrSelfDependency instead of tripping HasCycle (or the final\n\t\t// CycleThroughEdges gate) and surfacing as a cycle. The message is\n\t\t// byte-identical to every other self-dep site so the proxied bulk CLI\n\t\t// (bd dep add / bd link) shows one consistent self-dependency error.\n\t\tif dep.IssueID == dep.DependsOnID {\n\t\t\treturn BulkAddDepsResult{}, fmt.Errorf(\"%w: %s cannot depend on itself\", ErrSelfDependency, dep.IssueID)\n\t\t}\n\t}\n\tsources := make([]string, 0, len(deps))\n\tfor _, dep := range deps {\n\t\tsources = append(sources, dep.IssueID)\n\t}\n\t// One query for the batch, read before the first write. Nothing an edge\n\t// write does moves a source between planes, so the answer stays true for","sourceCodeStart":674,"sourceCodeEnd":710,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/dependency.go#L674-L710","documentation":"AddDependencies validates every edge in the batch before writing anything. This error means one Dependency struct in the deps slice had an empty IssueID or DependsOnID, so the whole bulk operation was aborted. Beads requires both endpoints of a dependency edge to be real issue identifiers; an empty endpoint would create a dangling edge in the Dolt-backed dependency table.","triggerScenarios":"Calling AddDependencies (via bd dep add / bd link bulk proxying or the SDK) with a Dependency whose IssueID or DependsOnID is the empty string, e.g. constructing edges from parsed input where a field was omitted or a split/parse produced an empty token.","commonSituations":"Scripts building dep lists from CSV/JSON where some rows have a blank column; shell loops interpolating an unset shell variable into an edge spec; SDK callers constructing types.Dependency structs manually and forgetting one endpoint.","solutions":["Check the deps slice entry at the index named in the message and populate both IssueID and DependsOnID before calling AddDependencies","Validate all edges client-side (non-empty, correctly formatted issue IDs) before building the batch","If input comes from parsing, filter out rows with empty endpoint fields instead of passing them through"],"exampleFix":"// before\nedge := &types.Dependency{IssueID: id, DependsOnID: fromFlag} // fromFlag empty\nres, err := uc.AddDependencies(ctx, edge, opts)\n// after\nif id == \"\" || fromFlag == \"\" { return errors.New(\"both endpoints required\") }\nedge := &types.Dependency{IssueID: id, DependsOnID: fromFlag}\nres, err := uc.AddDependencies(ctx, edge, opts)","handlingStrategy":"validation","validationCode":"for i, d := range deps {\n    if d == nil || d.IssueID == \"\" || d.DependsOnID == \"\" {\n        return fmt.Errorf(\"deps[%d]: both IssueID and DependsOnID are required\", i)\n    }\n}","typeGuard":"func validEdge(d *types.Dependency) bool {\n    return d != nil && d.IssueID != \"\" && d.DependsOnID != \"\"\n}","tryCatchPattern":null,"preventionTips":["Validate every edge endpoint before building the batch","Filter parsed/CSV input for empty fields upstream","Add a unit test that feeds an empty-endpoint edge and asserts a clean client-side rejection"],"tags":["validation","dependencies","bulk-operations"],"backgroundTag":"missing-required-field","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}