{"record":{"id":"ee8959bcb04ae2e8","repo":"gastownhall/beads","slug":"applygraph-node-d-key-q-has-nil-issue","errorCode":null,"errorMessage":"applyGraph: node %d (key=%q) has nil Issue","messagePattern":"applyGraph: node (.+?) \\(key=%q\\) has nil Issue","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/domain/issue.go","lineNumber":1116,"sourceCode":"\nfunc (u *issueUseCaseImpl) ApplyWispGraph(ctx context.Context, plan GraphPlan, actor string) (GraphApplyResult, error) {\n\treturn u.applyGraph(ctx, plan, actor, true)\n}\n\nfunc (u *issueUseCaseImpl) applyGraph(ctx context.Context, plan GraphPlan, actor string, useWisp bool) (GraphApplyResult, error) {\n\tkeyToID := make(map[string]string, len(plan.Nodes))\n\tpendingAssignees := make(map[int]string, len(plan.Nodes))\n\n\t// Pass 1 — create every node as a top-level issue. We deliberately do\n\t// not pass ParentID to u.create: graph nodes with parent_key/parent_id\n\t// receive top-level hash (or counter) IDs and have their parent linkage\n\t// added as a separate parent-child dep below. This matches embedded\n\t// executeGraphApply (cmd/bd/graph_apply.go) and lets children precede\n\t// parents in plan order — keyToID is only consulted after every node\n\t// has minted its ID.\n\tfor i, node := range plan.Nodes {\n\t\tif node.Issue == nil {\n\t\t\treturn GraphApplyResult{}, fmt.Errorf(\"applyGraph: node %d (key=%q) has nil Issue\", i, node.Key)\n\t\t}\n\t\t// The whole plan routes to one table, so every node's storage class\n\t\t// must match. The CLI pre-validates this; guard here too so other\n\t\t// callers cannot route wisp-flagged issues into the durable table.\n\t\tif nodeWisp := node.Issue.Ephemeral || node.Issue.NoHistory; nodeWisp != useWisp {\n\t\t\treturn GraphApplyResult{}, fmt.Errorf(\"applyGraph: node %q storage class (ephemeral=%t, no_history=%t) does not match plan routing (wisp=%t)\", node.Key, node.Issue.Ephemeral, node.Issue.NoHistory, useWisp)\n\t\t}\n\n\t\tif node.AssignAfterCreate {\n\t\t\tpendingAssignees[i] = node.Assignee\n\t\t\tnode.Issue.Assignee = \"\"\n\t\t} else if node.Assignee != \"\" {\n\t\t\tnode.Issue.Assignee = node.Assignee\n\t\t}\n\n\t\tparams := CreateIssueParams{\n\t\t\tIssue:  node.Issue,\n\t\t\tLabels: node.Labels,","sourceCodeStart":1098,"sourceCodeEnd":1134,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/domain/issue.go#L1098-L1134","documentation":"applyGraph validates that every plan node carries a non-nil Issue before minting IDs and inserting. A nil node.Issue means the plan was constructed incorrectly (e.g. a node added as a link-only placeholder without issue data). This is a caller/plan-construction bug, not a storage failure.","triggerScenarios":"Calling ApplyGraph with a plan whose Nodes[i].Issue is nil — typically from hand-built plans or a bug in plan generation that emits placeholder nodes.","commonSituations":"Custom tooling building GraphPlan structs directly; older plan files generated by a different bd version missing issue payloads; deserialized plans where issues were dropped.","solutions":["Regenerate the plan with the current bd CLI (bd graph apply plan) instead of hand-building it","Ensure every node added to plan.Nodes has a populated Issue struct","Skip/convert link-only placeholder nodes into proper dependency edges rather than plan nodes","Check for nil-Issue assignments when deserializing plan JSON"],"exampleFix":"// before: placeholder node with no issue\nplan.Nodes = append(plan.Nodes, GraphNode{Key: \"parent\"})\n// after: attach the issue payload\nplan.Nodes = append(plan.Nodes, GraphNode{Key: \"parent\", Issue: &types.Issue{Title: \"parent\", ...}})","handlingStrategy":"validation","validationCode":"for i, n := range plan.Nodes {\n    if n.Issue == nil {\n        return fmt.Errorf(\"plan node %d (key=%s) has nil Issue; regenerate plan\", i, n.Key)\n    }\n}","typeGuard":"func validNodes(plan GraphPlan) bool {\n    for _, n := range plan.Nodes {\n        if n.Issue == nil { return false }\n    }\n    return true\n}","tryCatchPattern":"result, err := uc.ApplyGraph(ctx, plan, actor)\nif err != nil && strings.Contains(err.Error(), \"has nil Issue\") {\n    plan = regeneratePlan(sourceFile) // rebuild from authoritative source\n    result, err = uc.ApplyGraph(ctx, plan, actor)\n}","preventionTips":["Always generate plans via the bd CLI rather than hand-building structs","Validate plan JSON after deserialization (nil issues, missing keys)","Skip or convert link-only placeholder nodes into dependency edges","Add a pre-apply plan lint step in CI"],"tags":["graph-apply","validation","plan","go"],"backgroundTag":"nil-plan-node","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}