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 twiceView on GitHub (pinned to 71377f2769)
Solutions
- Set ev.Ref to a non-empty string (pointer to the ref value) whenever ev.RefKind is set
- If no ref is available, either pass the event timestamp via OccurredAt and leave both RefKind and Ref nil, or skip recording the event
- 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
- Always set RefKind and Ref together via a constructor helper
- Never pass a pointer to an empty string as Ref
- Treat 'unknown ref-kind' vs 'requires a ref' errors as separate signal: first fix the kind, then the value
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
- no store is open for this workspace
- not found
- db: ChildCounterSQLRepository.NextChildID: parentID must not
- db: DependencySQLRepository.Insert: dep must not be nil
- db: DependencySQLRepository.Insert: IssueID must not be empt
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/7b022dbf51390bf0.
Report an issue: GitHub.