gastownhall/beads · error

invalid graph plan

Error message

invalid graph plan: %w

What it means

validateProxiedGraphPlan rejected the graph plan inside the transaction. The wrapped error says what is invalid: mixed storage classes, nonexistent dep targets, ID collisions, or malformed nodes. Nothing is committed — the transaction rolls back.

Solutions

  1. Fix the wrapped validation error in the graph file
  2. Ensure every dep target exists in the plan or the database
  3. Use a uniform storage class (all wisps or all issues) per plan
  4. De-duplicate explicit IDs across plan nodes

Example fix

// before: dep target not in plan or DB
{"nodes":[{"id":"bd-1","deps":[{"target":"bd-404"}]}]}
// after: reference an existing node
{"nodes":[{"id":"bd-1","deps":[{"target":"bd-2"}]},{"id":"bd-2"}]}
Defensive patterns

Strategy: validation

Validate before calling

// client-side pre-check before graph-apply
ids := map[string]bool{}
for _, n := range plan.Nodes {
    if ids[n.ID] { return fmt.Errorf("duplicate id %s", n.ID) }
    ids[n.ID] = true
}
for _, n := range plan.Nodes {
    for _, d := range n.Deps {
        if !ids[d.Target] && !existsOnServer(d.Target) {
            return fmt.Errorf("dep target %s not found", d.Target)
        }
    }
}

Prevention

When it happens

Trigger: runCreateProxiedGraph applies a plan whose nodes fail validation: inconsistent useWisp routing, dep on a nonexistent ID, duplicate explicit IDs, or invalid node fields (checked via uowIssueExists preflight).

Common situations: Hand-written or generated graph JSON referencing IDs not in the plan or DB; plan mixing wisps and issues; alias/dep typo in the graph file.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/652644237a6462bb. Report an issue: GitHub.

Appendix: source

Thrown at cmd/bd/create_proxied_server.go:345

	commitMsg := plan.CommitMessage
	if commitMsg == "" {
		commitMsg = fmt.Sprintf("bd: graph-apply %d nodes", len(plan.Nodes))
	}

	res, err := uow.RunTxResult(ctx, uowProvider, func(ctx context.Context, uw uow.UnitOfWork) (map[string]string, string, error) {
		cctx, err := uw.ConfigUseCase().LoadCreateContext(ctx)
		if err != nil {
			return nil, "", fmt.Errorf("load create context: %w", err)
		}

		// validateProxiedGraphPlan enforces a uniform storage class, so its
		// resolved useWisp decides which table the whole plan routes to. The
		// collision preflight runs inside this transaction, so it cannot race
		// a concurrent create of the same explicit ID.
		useWisp, err := validateProxiedGraphPlan(&plan, in, cctx, uowIssueExists(ctx, uw))
		if err != nil {
			return nil, "", fmt.Errorf("invalid graph plan: %w", err)
		}

		var result domain.GraphApplyResult
		var applyErr error
		if useWisp {
			result, applyErr = uw.IssueUseCase().ApplyWispGraph(ctx, domainPlan, in.createdBy)
		} else {
			result, applyErr = uw.IssueUseCase().ApplyIssueGraph(ctx, domainPlan, in.createdBy)
		}
		if applyErr != nil {
			return nil, "", fmt.Errorf("graph create: %w", applyErr)
		}

		return result.IDs, commitMsg, nil
	})
	if err != nil {
		return HandleError("%v", err)
	}

View on GitHub (pinned to 71377f2769)