{"record":{"id":"7031181815aaacda","repo":"gastownhall/beads","slug":"duplicate-step-ids-after-expansion-v","errorCode":null,"errorMessage":"duplicate step IDs after expansion: %v","messagePattern":"duplicate step IDs after expansion: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/formula/expand.go","lineNumber":160,"sourceCode":"\n\t\t\tresult = replaceStep(result, targetStep.ID, expandedSteps)\n\t\t\texpanded[targetStep.ID] = true\n\n\t\t\t// Update dependencies: any step that depended on the target should now\n\t\t\t// depend on the last step of the expansion\n\t\t\tif len(expandedSteps) > 0 {\n\t\t\t\tlastStepID := expandedSteps[len(expandedSteps)-1].ID\n\t\t\t\tresult = UpdateDependenciesForExpansion(result, targetStep.ID, lastStepID)\n\t\t\t}\n\n\t\t\t// Rebuild stepMap from result so subsequent iterations see resolved deps\n\t\t\tstepMap = buildStepMap(result)\n\t\t}\n\t}\n\n\t// Validate no duplicate step IDs after expansion\n\tif dups := findDuplicateStepIDs(result); len(dups) > 0 {\n\t\treturn nil, fmt.Errorf(\"duplicate step IDs after expansion: %v\", dups)\n\t}\n\n\treturn result, nil\n}\n\n// findDuplicateStepIDs returns any duplicate step IDs found in the steps slice.\n// It recursively checks all children.\nfunc findDuplicateStepIDs(steps []*Step) []string {\n\tseen := make(map[string]int)\n\tcountStepIDs(steps, seen)\n\n\tvar dups []string\n\tfor id, count := range seen {\n\t\tif count > 1 {\n\t\t\tdups = append(dups, id)\n\t\t}\n\t}\n\treturn dups","sourceCodeStart":142,"sourceCodeEnd":178,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/formula/expand.go#L142-L178","documentation":"After all expand/map rules are applied, ApplyExpansions runs findDuplicateStepIDs over the final step list. If two or more steps share an ID, the result would be ambiguous, so it fails with the list of duplicated IDs.","triggerScenarios":"ApplyExpansions final validation finds duplicates, typically because two map rules expanded steps into templates producing the same generated ID, or an expansion template produced IDs colliding with existing steps.","commonSituations":"Template IDs built from variables that resolve to the same value for different inputs; overlapping map select patterns expanding the same step twice; hardcoded template IDs colliding with static step IDs.","solutions":["Make template step IDs unique, e.g. include ${parent} or an input variable in the id","Narrow map select patterns so no step is expanded by two rules","Rename static steps that collide with template-generated IDs"],"exampleFix":"# before\nsteps:\n  - id: \"child\"       # collides for every expansion\n# after\nsteps:\n  - id: \"${parent}-child\"","handlingStrategy":"validation","validationCode":"ids := map[string]int{}\nfor _, s := range expandedStepsLocalSimulation { ids[s.ID]++ }\nfor id, n := range ids {\n    if n > 1 { return fmt.Errorf(\"would produce duplicate step id %q\", id) }\n}","typeGuard":null,"tryCatchPattern":"result, err := formula.ApplyExpansions(steps, compose)\nif err != nil && strings.Contains(err.Error(), \"duplicate step IDs\") {\n    return fmt.Errorf(\"fix template id variables to guarantee uniqueness: %w\", err)\n}","preventionTips":["Build template step IDs from unique inputs, e.g. ${parent}-${i}","Ensure map select patterns don't overlap","Add a pre-commit check simulating expansion ID generation"],"tags":["expansion","duplicate-ids","validation"],"backgroundTag":"duplicate-step-id","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}