{"record":{"id":"106f8a50fd3253db","repo":"gastownhall/beads","slug":"add-dep-cycle-check-w","errorCode":null,"errorMessage":"add dep: cycle check: %w","messagePattern":"add dep: cycle check: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/dependency.go","lineNumber":297,"sourceCode":"\t// Self-dependency guard mirrors issueops.CheckDependencyCycleInTx: it is\n\t// checked BEFORE the cycle probe and for ALL dep types, and emits the\n\t// dedicated self-dep message. A blocking self-edge otherwise trips HasCycle\n\t// and would report the wrong (cycle) error (#4547 F-1).\n\tif dep.IssueID == dep.DependsOnID {\n\t\treturn fmt.Errorf(\"%w: %s cannot depend on itself\", ErrSelfDependency, dep.IssueID)\n\t}\n\tif err := u.depRepo.ValidateBlockingHierarchy(ctx, dep); err != nil {\n\t\tvar hierarchyConflict *DependencyHierarchyConflictError\n\t\tif errors.As(err, &hierarchyConflict) {\n\t\t\treturn err\n\t\t}\n\t\treturn fmt.Errorf(\"add dep: hierarchy check: %w\", err)\n\t}\n\n\tif types.IsSchedulingEdge(dep.Type) {\n\t\tcycle, err := u.depRepo.HasCycle(ctx, dep.IssueID, dep.DependsOnID)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"add dep: cycle check: %w\", err)\n\t\t}\n\t\tif cycle {\n\t\t\t// Match the embedded store's user-facing wording verbatim (no ids\n\t\t\t// prefix) so gc code that string-matches this error behaves the same\n\t\t\t// on both plumbings (#4547 F-1).\n\t\t\treturn ErrDependencyCycle\n\t\t}\n\t}\n\n\tif err := u.depRepo.Insert(ctx, dep, actor, DepInsertOpts{UseWispsTable: useWisp, HierarchyValidated: true, CycleValidated: true, EmitEvent: true}); err != nil {\n\t\t// The retype conflict is a user-facing error whose message already\n\t\t// matches embedded verbatim; pass it through unwrapped so the CLI does\n\t\t// not prepend \"add dep: insert:\" (#4547 F-1). The endpoint-existence\n\t\t// refusals are here for the same reason.\n\t\tvar conflict *DependencyTypeConflictError\n\t\tif errors.As(err, &conflict) {\n\t\t\treturn err\n\t\t}","sourceCodeStart":279,"sourceCodeEnd":315,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/dependency.go#L279-L315","documentation":"Wraps a repository failure that occurred while running the cycle-detection probe (depRepo.HasCycle) during dependency creation. Beads refuses scheduling-type dependency edges (e.g. blocks/blocked-by) that would form a cycle, and this wrapper only fires when the probe itself errored — not when a cycle was actually found (that returns the sentinel ErrDependencyCycle instead). It means the cycle check could not complete, typically due to a storage/backend problem.","triggerScenarios":"Calling AddDependency or AddWispDependency with a dep whose Type passes types.IsSchedulingEdge (e.g. blocks, parent-child scheduling semantics), where the underlying HasCycle query to the Dolt/sql repo fails — DB connection dropped, SQL error, context canceled mid-query, or lock/timeout in the dependency graph query.","commonSituations":"Database unavailable or restarting while a script batches bd dep add calls; context deadline exceeded on large dependency graphs; transient Dolt server errors during sync; running commands against a stale or locked .beads database.","solutions":["Inspect the wrapped %w cause with errors.Unwrap / %+v to see the underlying repository error and fix that root cause first.","Verify the database backend is reachable and healthy (bd doctor, or reconnect the Dolt server).","Retry the operation once the storage layer is healthy — the check is read-only and safe to re-run.","If the cause is a context timeout, re-run with a longer deadline or smaller batch instead of bypassing the check."],"exampleFix":"// before: swallowing the wrapped cause\nif err := uc.AddDependency(ctx, dep, actor); err != nil {\n    log.Println(\"dep add failed\")\n}\n// after: surface and classify the underlying cause\nif err := uc.AddDependency(ctx, dep, actor); err != nil {\n    if errors.Is(err, domain.ErrDependencyCycle) {\n        log.Println(\"cycle rejected\")\n    } else {\n        log.Printf(\"cycle check/storage failure: %v\", err)\n    }\n}","handlingStrategy":"try-catch","validationCode":"if dep == nil || dep.IssueID == \"\" || dep.DependsOnID == \"\" || dep.IssueID == dep.DependsOnID {\n    return fmt.Errorf(\"invalid dependency before add\")\n}\nif !types.IsSchedulingEdge(dep.Type) {\n    // cycle check is skipped for non-scheduling edges\n    _ = dep\n}","typeGuard":"func isSchedulingDep(dep *types.Dependency) bool {\n    return dep != nil && dep.IssueID != \"\" && dep.DependsOnID != \"\" && types.IsSchedulingEdge(dep.Type)\n}","tryCatchPattern":"if err := uc.AddDependency(ctx, dep, actor); err != nil {\n    switch {\n    case errors.Is(err, domain.ErrDependencyCycle):\n        // cycle rejected — not this error\n    case errors.Is(err, context.DeadlineExceeded):\n        // storage timeout during cycle check; retry with longer deadline\n    default:\n        return fmt.Errorf(\"cycle check storage failure: %w\", err)\n    }\n}","preventionTips":["Pre-check client-side for obvious cycles (BFS on the scheduling graph) before issuing adds","Keep DB connections healthy; batch adds inside one healthy session","Use contexts with adequate deadlines for large graphs","Never swallow the wrapped cause — unwrap to find the storage root cause"],"tags":["go","storage","dependency-graph","cycle-check"],"backgroundTag":"dependency-cycle-check-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}