gastownhall/beads · error

provenance: source is required

Error message

provenance: source is required

What it means

Every provenance event must carry a non-empty Source identifying where it came from; ValidateProvenanceEvent rejects events whose Source is empty or whitespace-only. Source is distinct from actor/ref, which are opaque — Source is the structural 'who is reporting this' field.

Source

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

// read-first honesty filter can exclude backfilled rows. The record path rejects
// it (case-insensitively): real producers must name their own source.
const ReservedProvSource = "ingest-backfill"

var gitSHARE = regexp.MustCompile(`^[0-9a-f]{40}$`)

// ValidateProvenanceEvent checks the structural fields of a provenance event
// before it is recorded: kind, ref_kind (when present), the git-sha ref shape,
// and the reserved source. It never interprets the opaque actor/ref values. It
// is exported so the CLI can fail early with the same rules the store enforces.
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,

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set ev.Source to a stable identifier for the emitting component (e.g. "cli", "agent:foo", "ci")
  2. Add a construction helper that requires source, so it cannot be omitted
  3. Pre-validate with ValidateProvenanceEvent before recording to get a precise message
  4. Default source from context (command name / agent name) at the call site if it is genuinely unknown

Example fix

// before
ev := types.ProvenanceEvent{IssueID: id, Kind: types.ProvClaim, Actor: "alice"}
// after
ev := types.ProvenanceEvent{IssueID: id, Kind: types.ProvClaim, Actor: "alice", Source: "agent:alice"}
Defensive patterns

Strategy: validation

Validate before calling

if strings.TrimSpace(ev.Source) == "" {
	return errors.New("provenance event requires a source")
}
return issueops.ValidateProvenanceEvent(ev)

Type guard

func hasSource(ev types.ProvenanceEvent) bool { return strings.TrimSpace(ev.Source) != "" }

Try / catch

if err := issueops.RecordProvenanceEventInTx(ctx, tx, ev); err != nil {
	if strings.Contains(err.Error(), "source is required") {
		return fmt.Errorf("provenance event from %q missing source", ev.Actor)
	}
	return err
}

Prevention

When it happens

Trigger: Recording a provenance event with ev.Source == "" or " " via RecordProvenanceEventInTx or a direct ValidateProvenanceEvent call.

Common situations: Automation that fills actor but forgets source; refactored call sites where the source parameter was dropped; parsing events from external systems that lack a source field.

Related errors


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