{"record":{"id":"0b27c0819bff2fbf","repo":"gastownhall/beads","slug":"provenance-event-with-no-ref-requires-occurred-at","errorCode":null,"errorMessage":"provenance: event with no ref requires occurred_at (--at) for a stable id","messagePattern":"provenance: event with no ref requires occurred_at \\(--at\\) for a stable id","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/provenance.go","lineNumber":80,"sourceCode":"\t}\n\tif ev.RefKind != nil {\n\t\tif _, ok := knownProvRefKinds[*ev.RefKind]; !ok {\n\t\t\treturn fmt.Errorf(\"provenance: unknown ref-kind %q\", *ev.RefKind)\n\t\t}\n\t\tif ev.Ref == nil || *ev.Ref == \"\" {\n\t\t\treturn fmt.Errorf(\"provenance: ref-kind %q requires a ref\", *ev.RefKind)\n\t\t}\n\t\tif *ev.RefKind == \"git-sha\" {\n\t\t\tif !gitSHARE.MatchString(*ev.Ref) {\n\t\t\t\treturn fmt.Errorf(\"provenance: ref-kind git-sha requires a 40-character lowercase hex ref\")\n\t\t\t}\n\t\t}\n\t}\n\t// A ref-less event is keyed by occurred_at for its stable id; without either,\n\t// two distinct events would collapse to the same content-addressed id. Guard\n\t// at the store boundary so every caller (CLI or library) is covered.\n\tif (ev.Ref == nil || *ev.Ref == \"\") && ev.OccurredAt == nil {\n\t\treturn fmt.Errorf(\"provenance: event with no ref requires occurred_at (--at) for a stable id\")\n\t}\n\treturn nil\n}\n\n// ProvenanceEventID computes the deterministic, idempotent id for a provenance\n// event from source:issue:kind:(ref or occurred_at). A producer firing twice\n// with the same facts yields the same id, so the INSERT IGNORE in\n// RecordProvenanceEventInTx is a harmless no-op the second time. The\n// discriminator is the ref when present, otherwise the fixed-width occurred_at,\n// which is why a ref-less event requires --at (so the id is caller-owned, never\n// clock-derived at insert time). ValidateProvenanceEvent enforces that.\nfunc ProvenanceEventID(ev types.ProvenanceEvent) string {\n\tdisc := \"\"\n\tswitch {\n\tcase ev.Ref != nil && *ev.Ref != \"\":\n\t\tdisc = *ev.Ref\n\tcase ev.OccurredAt != nil:\n\t\t// Truncate to whole seconds: occurred_at is stored as bare DATETIME","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/provenance.go#L62-L98","documentation":"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.","triggerScenarios":"Calling RecordProvenanceEventInTx with an event that has neither Ref (nil or \"\") nor OccurredAt set.","commonSituations":"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.","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"],"exampleFix":"// before\nev := ProvenanceEvent{Source: \"cli\", IssueID: \"bd-1\"}\n// after\nnow := time.Now().UTC()\nev := ProvenanceEvent{Source: \"cli\", IssueID: \"bd-1\", OccurredAt: &now}","handlingStrategy":"validation","validationCode":"func provEventKeyable(ev ProvenanceEvent) bool {\n  hasRef := ev.Ref != nil && *ev.Ref != \"\"\n  return hasRef || ev.OccurredAt != nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["provenance","idempotency","validation"],"backgroundTag":"missing-timestamp","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}