{"record":{"id":"089da282ad01c500","repo":"gastownhall/beads","slug":"branch-from-step-q-not-found","errorCode":null,"errorMessage":"branch: from step %q not found","messagePattern":"branch: from step %q not found","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/formula/controlflow.go","lineNumber":457,"sourceCode":"\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]\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]","sourceCodeStart":439,"sourceCodeEnd":475,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/formula/controlflow.go#L439-L475","documentation":"applyBranchesWithMap verifies that the rule's From step exists in the step map built from the formula's steps. If the ID is set but does not match any step, expansion fails — this catches dangling references after renames or typos.","triggerScenarios":"ApplyBranches/ApplyControlFlow with a branch rule whose From names a step ID absent from the steps list (typo, renamed/deleted step, wrong casing).","commonSituations":"Renaming a step without updating the branch rule's from reference; copy-pasting branch config from another formula; case mismatch since step IDs are matched exactly.","solutions":["Update the branch rule's from to the current ID of the fan-out step","List the formula's step IDs and diff them against from/steps/join references","Restore the deleted step or remove the stale branch rule","Check for casing/whitespace differences in the ID"],"exampleFix":"# before\n- from: strat\n  steps: [a, b]\n  join: end\n# after\n- from: start\n  steps: [a, b]\n  join: end","handlingStrategy":"validation","validationCode":"func checkBranchFrom(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.From] {\n\t\t\treturn fmt.Errorf(\"branch from %q not found\", b.From)\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(), \"from step\") && strings.Contains(err.Error(), \"not found\") {\n\t\treturn fmt.Errorf(\"stale branch 'from' reference: %w\", err)\n\t}\n\treturn err\n}","preventionTips":["Cross-check branch from/steps/join IDs against step IDs in CI","Search for all references before renaming a step ID","Keep step IDs exact-match clean: no casing or whitespace drift"],"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"}