{"record":{"id":"25d9ab1b5a111ce3","repo":"gastownhall/beads","slug":"branch-join-step-q-not-found","errorCode":null,"errorMessage":"branch: join step %q not found","messagePattern":"branch: join step %q not found","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/formula/controlflow.go","lineNumber":460,"sourceCode":"\n\tfor _, branch := range compose.Branch {\n\t\t// Validate the branch rule\n\t\tif branch.From == \"\" {\n\t\t\treturn fmt.Errorf(\"branch: from is required\")\n\t\t}\n\t\tif len(branch.Steps) == 0 {\n\t\t\treturn fmt.Errorf(\"branch: steps is required\")\n\t\t}\n\t\tif branch.Join == \"\" {\n\t\t\treturn fmt.Errorf(\"branch: join is required\")\n\t\t}\n\n\t\t// Verify all steps exist\n\t\tif _, ok := stepMap[branch.From]; !ok {\n\t\t\treturn fmt.Errorf(\"branch: from step %q not found\", branch.From)\n\t\t}\n\t\tif _, ok := stepMap[branch.Join]; !ok {\n\t\t\treturn fmt.Errorf(\"branch: join step %q not found\", branch.Join)\n\t\t}\n\t\tfor _, stepID := range branch.Steps {\n\t\t\tif _, ok := stepMap[stepID]; !ok {\n\t\t\t\treturn fmt.Errorf(\"branch: parallel step %q not found\", stepID)\n\t\t\t}\n\t\t}\n\n\t\t// Add dependencies: branch steps depend on 'from'\n\t\tfor _, stepID := range branch.Steps {\n\t\t\tstep := stepMap[stepID]\n\t\t\tstep.Needs = appendUnique(step.Needs, branch.From)\n\t\t}\n\n\t\t// Add dependencies: 'join' depends on all branch steps\n\t\tjoinStep := stepMap[branch.Join]\n\t\tfor _, stepID := range branch.Steps {\n\t\t\tjoinStep.Needs = appendUnique(joinStep.Needs, stepID)\n\t\t}","sourceCodeStart":442,"sourceCodeEnd":478,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/formula/controlflow.go#L442-L478","documentation":"applyBranchesWithMap verifies the rule's Join step exists in the step map, just like From. A Join pointing at a nonexistent step ID means the convergence point is dangling and the branch cannot be applied.","triggerScenarios":"ApplyBranches/ApplyControlFlow with a branch rule whose Join names a step ID not present among the formula's steps.","commonSituations":"Join step deleted or renamed during refactoring; typo in the join ID; referring to a step defined inside a loop body that is not a top-level step at compose time.","solutions":["Update join to the current ID of the convergence step","Verify the join step is a top-level step (not nested inside a loop body)","Remove the branch rule if the join step no longer exists","Check casing/whitespace in the join ID"],"exampleFix":"# before\n- from: start\n  steps: [a, b]\n  join: finsh\n# after\n- from: start\n  steps: [a, b]\n  join: finish","handlingStrategy":"validation","validationCode":"func checkBranchJoin(steps []*Step, compose *formula.ComposeRules) error {\n\tids := map[string]bool{}\n\tfor _, s := range steps { ids[s.ID] = true }\n\tfor _, b := range compose.Branch {\n\t\tif !ids[b.Join] {\n\t\t\treturn fmt.Errorf(\"branch join %q not found\", b.Join)\n\t\t}\n\t}\n\treturn nil\n}","typeGuard":null,"tryCatchPattern":"steps, err := formula.ApplyBranches(steps, compose)\nif err != nil {\n\tif strings.Contains(err.Error(), \"join step\") && strings.Contains(err.Error(), \"not found\") {\n\t\treturn fmt.Errorf(\"stale branch 'join' reference: %w\", err)\n\t}\n\treturn err\n}","preventionTips":["Keep join steps at top level, not inside loop bodies","Cross-check join IDs against step IDs whenever steps change","Add a formula linter that validates all branch references exist"],"tags":["go","formula","branch","reference","compose"],"backgroundTag":"undefined-reference","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}