{"record":{"id":"6e24195f68dd7043","repo":"gastownhall/beads","slug":"childref-is-empty-after-variable-substitution","errorCode":null,"errorMessage":"childRef is empty after variable substitution","messagePattern":"childRef is empty after variable substitution","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/template.go","lineNumber":513,"sourceCode":"\n// generateBondedID creates a custom ID for dynamically bonded molecules.\n// When bonding a proto to a parent molecule, this generates IDs like:\n//   - Root: parent.childref (e.g., \"patrol-x7k.arm-ace\")\n//   - Children: parent.childref.step (e.g., \"patrol-x7k.arm-ace.capture\")\n//\n// The childRef is variable-substituted before use.\n// Returns empty string if not a bonded operation (opts.ParentID empty).\nfunc generateBondedID(oldID string, rootID string, opts CloneOptions) (string, error) {\n\tif opts.ParentID == \"\" {\n\t\treturn \"\", nil // Not a bonded operation\n\t}\n\n\t// Substitute variables in childRef\n\tchildRef := substituteVariables(opts.ChildRef, opts.Vars)\n\n\t// Validate childRef after substitution\n\tif childRef == \"\" {\n\t\treturn \"\", fmt.Errorf(\"childRef is empty after variable substitution\")\n\t}\n\tif !bondedIDPattern.MatchString(childRef) {\n\t\treturn \"\", fmt.Errorf(\"invalid childRef '%s': must be alphanumeric, dash, underscore, or dot only\", childRef)\n\t}\n\n\tif oldID == rootID {\n\t\t// Root issue: parent.childref\n\t\tnewID := fmt.Sprintf(\"%s.%s\", opts.ParentID, childRef)\n\t\treturn newID, nil\n\t}\n\n\t// Child issue: parent.childref.relative\n\t// Extract the relative portion of the old ID (part after root)\n\trelativeID := getRelativeID(oldID, rootID)\n\tif relativeID == \"\" {\n\t\t// No hierarchical relationship - use a suffix from the old ID to ensure uniqueness.\n\t\t// Extract the last part of the old ID (after any prefix or dash)\n\t\tsuffix := extractIDSuffix(oldID)","sourceCodeStart":495,"sourceCodeEnd":531,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/template.go#L495-L531","documentation":"generateBondedID builds a bonded molecule child ID by substituting template variables into opts.ChildRef. If substitution produces an empty string (e.g. the childRef was entirely a variable like '{{name}}' and that variable resolved to empty), it throws 'childRef is empty after variable substitution'. The guard exists because an empty childRef would yield malformed IDs like 'parent.'.","triggerScenarios":"Cloning/bonding a subgraph (cloneSubgraphInto or direct call) where CloneOptions.ChildRef consists only of variables that resolve to empty strings in opts.Vars — e.g. ChildRef='{{suffix}}' with Vars lacking 'suffix' or having an empty value.","commonSituations":"Template variables renamed in the proto but the caller still passes old variable names in Vars; an empty CLI flag value substituted in; Handlebars-style {{var}} left unresolved and stripped to empty.","solutions":["Pass a non-empty value for the variable used in ChildRef in CloneOptions.Vars.","Set a literal (non-variable) ChildRef if no dynamic naming is needed.","Check the proto template for which variables it expects in childRef and provide them all.","If a variable may be empty, give ChildRef a static prefix, e.g. 'task-{{n}}' instead of '{{n}}'."],"exampleFix":"// before\nopts := CloneOptions{ChildRef: \"{{suffix}}\", Vars: map[string]string{}}\n// after\nopts := CloneOptions{ChildRef: \"{{suffix}}\", Vars: map[string]string{\"suffix\": \"step1\"}}","handlingStrategy":"validation","validationCode":"resolved := substituteVariables(opts.ChildRef, opts.Vars)\nif resolved == \"\" {\n\treturn fmt.Errorf(\"childRef resolves to empty; provide vars for %s\", opts.ChildRef)\n}","typeGuard":null,"tryCatchPattern":"newID, err := generateBondedID(ctx, tx, oldID, rootID, opts)\nif err != nil && strings.Contains(err.Error(), \"childRef is empty\") {\n\treturn fmt.Errorf(\"provide a value for the childRef variable: %w\", err)\n}","preventionTips":["Ensure every variable used in ChildRef has a non-empty entry in CloneOptions.Vars.","Prefer mixed literal+variable childRefs ('task-{{n}}') over pure variables.","Validate Vars keys against the template's extractVariables output before cloning."],"tags":["template","variables","validation","bonding"],"backgroundTag":"template-variable-empty","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}