gastownhall/beads · error
provenance: event with no ref requires occurred_at (--at) fo
Error message
provenance: event with no ref requires occurred_at (--at) for a stable id
What it means
A provenance event with no ref at all is keyed by its occurred_at timestamp for its deterministic, idempotent id (source:issue:kind:(ref or occurred_at)). If both Ref and OccurredAt are nil, two distinct events would collapse to the same content-addressed id. The store rejects this combination so every event has a stable unique id.
Source
Thrown at internal/storage/issueops/provenance.go:80
}
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
// with the same facts yields the same id, so the INSERT IGNORE in
// RecordProvenanceEventInTx is a harmless no-op the second time. The
// discriminator is the ref when present, otherwise the fixed-width occurred_at,
// which is why a ref-less event requires --at (so the id is caller-owned, never
// clock-derived at insert time). ValidateProvenanceEvent enforces that.
func ProvenanceEventID(ev types.ProvenanceEvent) string {
disc := ""
switch {
case ev.Ref != nil && *ev.Ref != "":
disc = *ev.Ref
case ev.OccurredAt != nil:
// Truncate to whole seconds: occurred_at is stored as bare DATETIMEView on GitHub (pinned to 71377f2769)
Solutions
- Set OccurredAt (or pass --at on the CLI) to the event's timestamp when no ref applies
- Provide a Ref instead if the event is tied to a git ref
- Check the event in caller code: if Ref is empty, require OccurredAt non-nil before calling
Example fix
// before
ev := ProvenanceEvent{Source: "cli", IssueID: "bd-1"}
// after
now := time.Now().UTC()
ev := ProvenanceEvent{Source: "cli", IssueID: "bd-1", OccurredAt: &now} Defensive patterns
Strategy: validation
Validate before calling
func provEventKeyable(ev ProvenanceEvent) bool {
hasRef := ev.Ref != nil && *ev.Ref != ""
return hasRef || ev.OccurredAt != nil
} Prevention
- Default OccurredAt to time.Now().UTC() when building ref-less events
- Remember --at on CLI invocations that record ref-less events
- Check keyability before calling the store to avoid wasted transactions
When it happens
Trigger: Calling RecordProvenanceEventInTx with an event that has neither Ref (nil or "") nor OccurredAt set.
Common situations: Recording a generic event not tied to any git ref and forgetting the --at flag on the CLI or the OccurredAt field in the library call; constructing events in tests without timestamps.
Related errors
- provenance: issue id is required
- provenance: unknown kind %q
- provenance: source is required
- provenance: source %q is reserved for ingest backfill and ca
- provenance: unknown ref-kind %q
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/0b27c0819bff2fbf.
Report an issue: GitHub.