gastownhall/beads · error

provenance: ref-kind %q requires a ref

Error message

provenance: ref-kind %q requires a ref

What it means

ValidateProvenanceEvent rejects a provenance event whose RefKind is a known kind but whose Ref pointer is nil or empty. Provenance events are content-addressed by source:issue:kind:(ref or occurred_at); a declared ref-kind without an actual ref value would produce an event that cannot be keyed or traced. The store refuses such events at the boundary so all callers (CLI or library) get a consistent error.

Source

Thrown at internal/storage/issueops/provenance.go:68

func ValidateProvenanceEvent(ev types.ProvenanceEvent) error {
	if strings.TrimSpace(ev.IssueID) == "" {
		return fmt.Errorf("provenance: issue id is required")
	}
	if _, ok := knownProvKinds[ev.Kind]; !ok {
		return fmt.Errorf("provenance: unknown kind %q", ev.Kind)
	}
	if strings.TrimSpace(ev.Source) == "" {
		return fmt.Errorf("provenance: source is required")
	}
	if strings.EqualFold(strings.TrimSpace(ev.Source), ReservedProvSource) {
		return fmt.Errorf("provenance: source %q is reserved for ingest backfill and cannot be recorded directly", ReservedProvSource)
	}
	if ev.RefKind != nil {
		if _, ok := knownProvRefKinds[*ev.RefKind]; !ok {
			return fmt.Errorf("provenance: unknown ref-kind %q", *ev.RefKind)
		}
		if ev.Ref == nil || *ev.Ref == "" {
			return fmt.Errorf("provenance: ref-kind %q requires a ref", *ev.RefKind)
		}
		if *ev.RefKind == "git-sha" {
			if !gitSHARE.MatchString(*ev.Ref) {
				return fmt.Errorf("provenance: ref-kind git-sha requires a 40-character lowercase hex ref")
			}
		}
	}
	// A ref-less event is keyed by occurred_at for its stable id; without either,
	// two distinct events would collapse to the same content-addressed id. Guard
	// at the store boundary so every caller (CLI or library) is covered.
	if (ev.Ref == nil || *ev.Ref == "") && ev.OccurredAt == nil {
		return fmt.Errorf("provenance: event with no ref requires occurred_at (--at) for a stable id")
	}
	return nil
}

// ProvenanceEventID computes the deterministic, idempotent id for a provenance
// event from source:issue:kind:(ref or occurred_at). A producer firing twice

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set ev.Ref to a non-empty string (pointer to the ref value) whenever ev.RefKind is set
  2. If no ref is available, either pass the event timestamp via OccurredAt and leave both RefKind and Ref nil, or skip recording the event
  3. Add a caller-side check that RefKind and Ref are set together before invoking the store

Example fix

// before
ev := ProvenanceEvent{RefKind: strPtr("branch")}
// after
ref := "feature/fix-xyz"
ev := ProvenanceEvent{RefKind: strPtr("branch"), Ref: &ref}
Defensive patterns

Strategy: validation

Validate before calling

func validProvRef(ev ProvenanceEvent) bool {
  return ev.RefKind == nil || (ev.Ref != nil && *ev.Ref != "")
}

Prevention

When it happens

Trigger: Calling RecordProvenanceEventInTx (which runs ValidateProvenanceEvent) with an event that sets RefKind to a known kind (e.g. "branch", "git-sha") but leaves Ref nil or sets it to "".

Common situations: Building a ProvenanceEvent struct programmatically and populating RefKind from config while forgetting to thread the actual git ref/branch value through; passing a pointer to an empty string because the ref was resolved too late or an env lookup failed silently.

Related errors


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