{"record":{"id":"1f1a48add211251b","repo":"gastownhall/beads","slug":"dependency-cycle-would-be-created-s-no-edges-ad","errorCode":null,"errorMessage":"dependency cycle would be created: %s (no edges added; run 'bd dep cycles' for analysis)","messagePattern":"dependency cycle would be created: (.+?) \\(no edges added; run 'bd dep cycles' for analysis\\)","errorType":"validation","errorClass":"domain.CycleError","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/dependency_editor.go","lineNumber":231,"sourceCode":"// already prints.\nfunc checkAddedEdgesForCycles(ctx context.Context, tx *sql.Tx, edges []publicops.DependencyEdge) error {\n\tvar pairs [][2]string\n\tfor _, edge := range edges {\n\t\tif !types.IsSchedulingEdge(edge.Type) {\n\t\t\tcontinue\n\t\t}\n\t\tpairs = append(pairs, [2]string{edge.IssueID, edge.DependsOnID})\n\t}\n\tif len(pairs) == 0 {\n\t\treturn nil\n\t}\n\tgraph := make(map[string][]string)\n\tif err := AppendSchedulingGraphInTx(ctx, tx, cycleDetectionTables(), graph); err != nil {\n\t\treturn fmt.Errorf(\"final cycle check failed (no edges added): %w\", err)\n\t}\n\tif cyclePath := CycleThroughEdgesInGraph(graph, pairs); cyclePath != \"\" {\n\t\treturn domain.NewCycleError(\"dependency cycle would be created: %s (no edges added; run 'bd dep cycles' for analysis)\", cyclePath)\n\t}\n\treturn nil\n}\n\n// ExecuteRemoveDependency removes one edge in tx and reports the durable\n// tables changed.\n//\n// A missing edge reports Removed false and NO changed tables, which is how the\n// callers spell \"commit nothing\": removing an edge that was never there leaves\n// the graph it already had, and a history entry for it would be a commit with\n// nothing in it.\nfunc ExecuteRemoveDependency(ctx context.Context, tx *sql.Tx, request publicops.RemoveDependencyRequest) (publicops.RemoveDependencyResult, ChangedTables, error) {\n\t// Routing is READ here rather than pinned, unlike the add. A removal\n\t// cannot put an edge anywhere, so pinning it would only mean failing to\n\t// remove an edge the caller named — and the staging has to name the tables\n\t// the delete actually touched or the commit sweeps rows it never wrote\n\t// (GH#2455). ChangedTables drops the wisp tables itself.\n\tsourceIsWisp := IsActiveWispInTx(ctx, tx, request.IssueID)\n\t_, _, eventTable, depTable := WispTableRouting(sourceIsWisp)","sourceCodeStart":213,"sourceCodeEnd":249,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/dependency_editor.go#L213-L249","documentation":"The final in-transaction cycle check found that asserting the requested dependency edges would create a scheduling cycle, so ExecuteAddDependencies aborts and no edges are added. domain.NewCycleError produces a typed error carrying the cycle path, and the message suggests running 'bd dep cycles' to analyze existing cycles. This is a deliberate domain rule: the dependency graph must remain acyclic.","triggerScenarios":"Calling ExecuteAddDependencies (or the runEndGate path) where CycleThroughEdgesInGraph finds a path through the existing graph plus the new pairs that loops back to a node — e.g. adding bd-1 depends-on bd-2 when bd-2 already (transitively) depends on bd-1.","commonSituations":"Bulk-adding dependencies where one pair in the batch closes a loop with another pair; concurrent agents each adding opposite-direction edges; re-adding edges after a prior partial import created a near-cycle; users manually wiring 'blocked by' in both directions.","solutions":["Read the cycle path from the error and remove/reverse one of the listed edges so the chain no longer loops.","Run 'bd dep cycles' as the message suggests to inspect and clean up existing cycles before retrying.","If a batch add fails, split it and add pairs one at a time (or pre-validate the batch offline against the current graph) to isolate the offending pair.","If the two issues should truly block each other, model it with a different mechanism (e.g. a single directional edge plus a related/discovered link) instead of mutual depends-on edges."],"exampleFix":"// before: closes a cycle\nExecuteAddDependencies(ctx, req{pairs: [{A, B}]}) // B already transitively depends on A\n\n// after: assert the opposite direction or drop the edge\n// bd dep remove B --depends-on A   (or skip the pair)\nExecuteAddDependencies(ctx, req{pairs: [{B, A}]})","handlingStrategy":"try-catch","validationCode":"// pre-flight outside the tx: check the pair doesn't close a loop\n// bd dep cycles  (CLI) or load the graph and test reachability\nfunc wouldCycle(graph map[string][]string, from, to string) bool {\n    seen := map[string]bool{}\n    var dfs func(string) bool\n    dfs = func(n string) bool {\n        if n == from { return true }\n        if seen[n] { return false }\n        seen[n] = true\n        for _, m := range graph[n] { if dfs(m) { return true } }\n        return false\n    }\n    return dfs(to)\n}","typeGuard":"func isCycleError(err error) bool {\n    var ce *domain.CycleError\n    return errors.As(err, &ce)\n}","tryCatchPattern":"err := editor.ExecuteAddDependencies(ctx, req)\nvar ce *domain.CycleError\nif errors.As(err, &ce) {\n    // surface ce's cycle path to the user; suggest 'bd dep cycles'\n    return fmt.Errorf(\"cannot add dependency: %v\", ce)\n}\nif err != nil { return err }","preventionTips":["Run 'bd dep cycles' before large dependency imports and clean existing cycles first.","Pre-validate each new edge for reachability against the current graph in tooling that adds edges programmatically.","When two issues mutually block each other, use one directional edge plus a related link instead of both directions.","When batch-adding, sort pairs and detect the first offending pair by adding one at a time."],"tags":["go","dependencies","cycle-detection","domain-rule","graph"],"backgroundTag":"dependency-cycle-detected","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}