{"record":{"id":"9e01e2857a198a7c","repo":"gastownhall/beads","slug":"circular-extends-detected-s","errorCode":null,"errorMessage":"circular extends detected: %s","messagePattern":"circular extends detected: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/formula/parser.go","lineNumber":205,"sourceCode":"\t// Set defaults\n\tif formula.Version == 0 {\n\t\tformula.Version = 1\n\t}\n\tif formula.Type == \"\" {\n\t\tformula.Type = TypeWorkflow\n\t}\n\n\treturn &formula, nil\n}\n\n// Resolve fully resolves a formula, processing extends and expansions.\n// Returns a new formula with all inheritance applied.\nfunc (p *Parser) Resolve(formula *Formula) (*Formula, error) {\n\t// Check for cycles\n\tif p.resolvingSet[formula.Formula] {\n\t\t// Build the cycle chain for a clear error message\n\t\tchain := append(p.resolvingChain, formula.Formula)\n\t\treturn nil, fmt.Errorf(\"circular extends detected: %s\", strings.Join(chain, \" -> \"))\n\t}\n\tp.resolvingSet[formula.Formula] = true\n\tp.resolvingChain = append(p.resolvingChain, formula.Formula)\n\tdefer func() {\n\t\tdelete(p.resolvingSet, formula.Formula)\n\t\tp.resolvingChain = p.resolvingChain[:len(p.resolvingChain)-1]\n\t}()\n\n\t// If no extends, just validate and return\n\tif len(formula.Extends) == 0 {\n\t\tif err := formula.Validate(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn formula, nil\n\t}\n\n\t// Build merged formula from parents\n\tmerged := &Formula{","sourceCodeStart":187,"sourceCodeEnd":223,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/formula/parser.go#L187-L223","documentation":"Resolve() detects that the formula currently being resolved is already in the active inheritance chain (p.resolvingSet), meaning formula A extends B which extends A (directly or transitively). The library refuses to recurse infinitely and reports the full cycle chain joined with ' -> ' so the offending loop is visible. Formula inheritance must form a DAG, not a cycle.","triggerScenarios":"Calling Parser.Resolve on a formula whose Extends chain loops back to an ancestor, e.g. formula 'deploy' has 'extends: [staging]' and 'staging' has 'extends: [deploy]'. Also triggered when a formula extends itself ('extends: [deploy]' inside deploy.md).","commonSituations":"Copy-pasting an existing formula and forgetting to update its extends list; renaming formulas so an old self-reference now points back; two teams each making the other formula their parent during a refactor; merging formula directories where both files claim the other as base.","solutions":["Read the chain in the error message (A -> B -> A) and remove the last extends link that closes the loop in one of the named formulas.","If both formulas genuinely need each other's vars, extract the shared vars into a third base formula and have both extend that.","If a rename caused the loop, grep the formulas directory for the old name and fix stale extends entries."],"exampleFix":"# before: staging.md\nextends: [deploy]\n# and deploy.md\nextends: [staging]\n\n# after: deploy.md holds the base vars;\n# staging.md\nextends: [deploy]  # deploy.md no longer extends staging","handlingStrategy":"validation","validationCode":"// Build a parent map and detect cycles before calling Resolve.\nfunc hasCycle(name string, extends map[string][]string, seen map[string]bool, chain []string) bool {\n\tif seen[name] {\n\t\tfmt.Fprintf(os.Stderr, \"cycle: %s -> %s\\n\", strings.Join(chain, \" -> \"), name)\n\t\treturn true\n\t}\n\tseen[name] = true\n\tfor _, parent := range extends[name] {\n\t\tif hasCycle(parent, extends, seen, append(chain, name)) {\n\t\t\treturn true\n\t\t}\n\t}\n\tdelete(seen, name)\n\treturn false\n}","typeGuard":null,"tryCatchPattern":"if _, err := p.Resolve(f); err != nil {\n\tif strings.Contains(err.Error(), \"circular extends detected\") {\n\t\t// surface the chain from the message\n\t}\n}","preventionTips":["Keep inheritance depth shallow and directed: base formulas should never extend their children.","After renaming a formula, grep all extends entries for the old name.","Add a CI check that runs Resolve over every formula to catch cycles at commit time."],"tags":["go","formula","inheritance","cycle-detection"],"backgroundTag":"circular-inheritance","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}