{"record":{"id":"f53f0cb07d224dcd","repo":"gastownhall/beads","slug":"branch-join-is-required","errorCode":null,"errorMessage":"branch: join is required","messagePattern":"branch: join is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/formula/controlflow.go","lineNumber":452,"sourceCode":"\n// applyBranchesWithMap applies branch rules using a pre-built stepMap.\n// This is the internal implementation used by both ApplyBranches and ApplyControlFlow.\n// The stepMap entries are modified in place.\nfunc applyBranchesWithMap(stepMap map[string]*Step, compose *ComposeRules) error {\n\tif compose == nil || len(compose.Branch) == 0 {\n\t\treturn nil\n\t}\n\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]","sourceCodeStart":434,"sourceCodeEnd":470,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/formula/controlflow.go#L434-L470","documentation":"Each branch rule must name a Join step where the parallel paths converge. applyBranchesWithMap rejects rules with an empty Join because the library cannot reconstruct execution ordering without a convergence point.","triggerScenarios":"ApplyBranches or ApplyControlFlow encountering a compose.Branch entry whose Join field is empty/omitted.","commonSituations":"YAML branch block missing `join:`; assuming join defaults to the last step (it does not); programmatic construction of ComposeRules leaving Join unset.","solutions":["Add `join:` naming the convergence step in each branch rule","Confirm the join step is a real step ID in the formula (checked immediately after)","Re-check YAML indentation so join belongs to the same branch list item"],"exampleFix":"# before\nbranch:\n  - from: start\n    steps: [a, b]\n# after\nbranch:\n  - from: start\n    steps: [a, b]\n    join: end","handlingStrategy":"validation","validationCode":"for i, b := range compose.Branch {\n\tif b.Join == \"\" {\n\t\treturn fmt.Errorf(\"branch[%d]: join is required\", i)\n\t}\n}","typeGuard":null,"tryCatchPattern":"steps, err := formula.ApplyBranches(steps, compose)\nif err != nil {\n\tif strings.Contains(err.Error(), \"join is required\") {\n\t\treturn fmt.Errorf(\"compose branch missing 'join': %w\", err)\n\t}\n\treturn err\n}","preventionTips":["Always specify the convergence step explicitly; there is no default join","Validate compose rules when formulas are loaded, not at expansion time","Check indentation so join belongs to the branch list item"],"tags":["go","formula","branch","validation","compose"],"backgroundTag":"missing-required-field","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}