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 DATETIME

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set OccurredAt (or pass --at on the CLI) to the event's timestamp when no ref applies
  2. Provide a Ref instead if the event is tied to a git ref
  3. 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

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


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