{"record":{"id":"c8de0b475d1e5108","repo":"gastownhall/beads","slug":"legacy-sqlite-dependency-graph-has-a-scheduling-cy","errorCode":null,"errorMessage":"legacy SQLite dependency graph has a scheduling cycle","messagePattern":"legacy SQLite dependency graph has a scheduling cycle","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/migration/legacysqlite/reader.go","lineNumber":1007,"sourceCode":"\thierarchy := make(map[string][]string)\n\tvar blocking []*types.Dependency\n\tfor _, issue := range issues {\n\t\tfor _, dep := range issue.Dependencies {\n\t\t\tif dep.IssueID == dep.DependsOnID {\n\t\t\t\treturn fmt.Errorf(\"dependency %s -> %s is a self-dependency\", dep.IssueID, dep.DependsOnID)\n\t\t\t}\n\t\t\tswitch dep.Type {\n\t\t\tcase types.DepBlocks, types.DepConditionalBlocks:\n\t\t\t\tblocking = append(blocking, dep)\n\t\t\t\tscheduling[dep.IssueID] = append(scheduling[dep.IssueID], dep.DependsOnID)\n\t\t\tcase types.DepParentChild:\n\t\t\t\thierarchy[dep.IssueID] = append(hierarchy[dep.IssueID], dep.DependsOnID)\n\t\t\t\tscheduling[dep.IssueID] = append(scheduling[dep.IssueID], dep.DependsOnID)\n\t\t\t}\n\t\t}\n\t}\n\tif hasDirectedCycle(scheduling) {\n\t\treturn fmt.Errorf(\"legacy SQLite dependency graph has a scheduling cycle\")\n\t}\n\tfor _, dep := range blocking {\n\t\tif types.ExtractPrefix(dep.IssueID) == types.ExtractPrefix(dep.DependsOnID) &&\n\t\t\t(reachable(hierarchy, dep.IssueID, dep.DependsOnID) ||\n\t\t\t\treachable(hierarchy, dep.DependsOnID, dep.IssueID)) {\n\t\t\treturn fmt.Errorf(\"blocking dependency %s -> %s conflicts with parent-child hierarchy\", dep.IssueID, dep.DependsOnID)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc hasDirectedCycle(graph map[string][]string) bool {\n\tindegree := make(map[string]int, len(graph))\n\tfor from, targets := range graph {\n\t\tif _, ok := indegree[from]; !ok {\n\t\t\tindegree[from] = 0\n\t\t}\n\t\tfor _, target := range targets {","sourceCodeStart":989,"sourceCodeEnd":1025,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/migration/legacysqlite/reader.go#L989-L1025","documentation":"The reader builds a scheduling edge map from blocking-type dependencies (blocks and conditional-blocks) and runs a directed-cycle detection. A scheduling cycle means a group of issues each block each other transitively, so no valid execution order exists; migration aborts rather than importing an unsatisfiable graph.","triggerScenarios":"Legacy SQLite database whose blocking dependencies form a directed cycle, e.g. bd-a blocks bd-b, bd-b blocks bd-c, bd-c blocks bd-a (any length loop through DepBlocks/DepConditionalBlocks edges).","commonSituations":"Long-lived legacy databases where overlapping 'blocks' relations accumulated into loops; bulk imports or scripts that added blocking deps without cycle checks; merges of several legacy DBs.","solutions":["Identify the cycle members from the migration log (they appear in the scheduling edges) and remove or retype one dependency to break the loop: DELETE FROM dependencies WHERE issue_id='bd-c' AND depends_on_id='bd-a'","Convert non-essential blocking edges to a non-scheduling type (e.g. related) in the legacy DB","If the cycle reflects reality, merge the issues or re-plan dependencies so a topological order exists, then re-run migration"],"exampleFix":"-- before\n-- bd-a -> bd-b -> bd-c -> bd-a (blocks)\nDELETE FROM dependencies WHERE issue_id = 'bd-c' AND depends_on_id = 'bd-a';\n-- after: scheduling graph is acyclic","handlingStrategy":"validation","validationCode":"function hasCycle(edges) {\n  const state = {};\n  function dfs(n) {\n    if (state[n] === 1) return true;\n    if (state[n] === 2) return false;\n    state[n] = 1;\n    for (const m of edges[n] || []) if (dfs(m)) return true;\n    state[n] = 2;\n    return false;\n  }\n  return Object.keys(edges).some(dfs);\n}\nconst scheduling = {};\nfor (const d of legacyDeps.filter(d => ['blocks','conditional-blocks'].includes(d.type)))\n  (scheduling[d.issue_id] ||= []).push(d.depends_on_id);\nif (hasCycle(scheduling)) throw new Error('scheduling cycle in legacy blocking deps');","typeGuard":"function schedulingGraphAcyclic(deps) {\n  const edges = {};\n  for (const d of deps.filter(d => d.type === 'blocks' || d.type === 'conditional-blocks'))\n    (edges[d.issue_id] ||= []).push(d.depends_on_id);\n  return !hasCycle(edges);\n}","tryCatchPattern":"try { migrateLegacySQLite(dbPath) } catch (e) {\n  if (e.message.includes('scheduling cycle')) {\n    const cycle = findSchedulingCycle(dbPath); // DFS over blocking edges\n    breakOrRetypeCycleEdge(dbPath, cycle);\n    retry();\n  } else throw e;\n}","preventionTips":["Run cycle detection on blocking edges before every migration","Retype non-essential blocking deps to a non-scheduling type in the legacy DB","Prevent cycle-creating inserts in tooling by checking reachability before adding a blocks edge"],"tags":["migration","sqlite","cycle-detection","graph-validation"],"backgroundTag":"dependency-cycle","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}