gastownhall/beads · error · issueops.ErrValidation

%w: apply batch item %d: %s names both key %q and id %q; exa

Error message

%w: apply batch item %d: %s names both key %q and id %q; exactly one

What it means

validateApplyRef enforces that each batch ref names an issue by exactly one identifier. This error fires when both ref.Key and ref.ID are set, which would be ambiguous — the applier cannot know which identifier is authoritative.

Source

Thrown at internal/storage/batch_apply.go:326

		return nil
	}
	declaredAt, ok := keyIndex[ref.Key]
	if !ok {
		return &issueops.RefError{Index: index, Member: member, Key: ref.Key}
	}
	if declaredAt >= index {
		return &issueops.RefError{Index: index, Member: member, Key: ref.Key, DeclaredLater: true}
	}
	return nil
}

// validateApplyRef checks the exactly-one rule every ref answers to.
func validateApplyRef(ref issueops.Ref, index int, member string) error {
	switch {
	case ref.Key == "" && ref.ID == "":
		return fmt.Errorf("%w: apply batch item %d: %s must name a key or an id", issueops.ErrValidation, index, member)
	case ref.Key != "" && ref.ID != "":
		return fmt.Errorf("%w: apply batch item %d: %s names both key %q and id %q; exactly one",
			issueops.ErrValidation, index, member, ref.Key, ref.ID)
	}
	return nil
}

// applyTouchKeyRef renders a ref as the address the touched set is keyed by.
// The two namespaces are kept apart so a key and an id that happen to read the
// same string are not confused for one row.
func applyTouchKeyRef(ref issueops.Ref) string {
	if ref.Key != "" {
		return "key:" + ref.Key
	}
	return "id:" + ref.ID
}

// applyRefLabel renders a ref for a message.
func applyRefLabel(ref issueops.Ref) string {
	if ref.Key != "" {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Clear one of the two fields so exactly one identifier remains (prefer Key for readability, ID for stability)
  2. Add client-side normalization that strips ID when Key is present (or vice versa)
  3. Fix the code that populates the Ref to set only one field

Example fix

// before
ref := issueops.Ref{Key: "beads-42", ID: "bd-a1b2c3"}
// after
ref := issueops.Ref{Key: "beads-42"}
Defensive patterns

Strategy: validation

Validate before calling

func exactlyOne(r issueops.Ref) error {
	if r.Key != "" && r.ID != "" {
		return fmt.Errorf("ref sets both key %q and id %q", r.Key, r.ID)
	}
	return nil
}

Type guard

func refUnambiguous(r issueops.Ref) bool { return (r.Key == "") != (r.ID == "") }

Prevention

When it happens

Trigger: Passing a Ref with both Key and ID non-empty to a batch apply item (create path or target ref validation).

Common situations: Copying a fully-populated issue struct into a Ref instead of picking one field; merging payloads where one source supplied key and another supplied id; ORM lookups that fill both fields.

Related errors


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