{"record":{"id":"41588bf2affb8149","repo":"gastownhall/beads","slug":"provenance-source-q-is-reserved-for-ingest-backf","errorCode":null,"errorMessage":"provenance: source %q is reserved for ingest backfill and cannot be recorded directly","messagePattern":"provenance: source %q is reserved for ingest backfill and cannot be recorded directly","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/provenance.go","lineNumber":61,"sourceCode":"\nvar gitSHARE = regexp.MustCompile(`^[0-9a-f]{40}$`)\n\n// ValidateProvenanceEvent checks the structural fields of a provenance event\n// before it is recorded: kind, ref_kind (when present), the git-sha ref shape,\n// and the reserved source. It never interprets the opaque actor/ref values. It\n// is exported so the CLI can fail early with the same rules the store enforces.\nfunc ValidateProvenanceEvent(ev types.ProvenanceEvent) error {\n\tif strings.TrimSpace(ev.IssueID) == \"\" {\n\t\treturn fmt.Errorf(\"provenance: issue id is required\")\n\t}\n\tif _, ok := knownProvKinds[ev.Kind]; !ok {\n\t\treturn fmt.Errorf(\"provenance: unknown kind %q\", ev.Kind)\n\t}\n\tif strings.TrimSpace(ev.Source) == \"\" {\n\t\treturn fmt.Errorf(\"provenance: source is required\")\n\t}\n\tif strings.EqualFold(strings.TrimSpace(ev.Source), ReservedProvSource) {\n\t\treturn fmt.Errorf(\"provenance: source %q is reserved for ingest backfill and cannot be recorded directly\", ReservedProvSource)\n\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 {","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/provenance.go#L43-L79","documentation":"Certain source values are reserved for system use: ReservedProvSource (the ingest backfill marker) cannot be attached to directly recorded events, so consumers can use it as an honesty filter to exclude derived/reconstructed rows. ValidateProvenanceEvent rejects any event whose Source equals this reserved value (case-insensitively).","triggerScenarios":"Calling RecordProvenanceEventInTx with ev.Source set (in any casing) to the value of issueops.ReservedProvSource; importing/backfilling events by writing them through the normal record path instead of the ingest backfill path.","commonSituations":"Import scripts copying exported events verbatim (including backfill rows) and re-recording them; tooling that marks its own events as 'backfill' or similar to seem system-generated; reconstruction tools writing derived events through the wrong API.","solutions":["Choose a distinct, non-reserved Source value for your events (e.g. \"import:mytool\")","Use the dedicated ingest/backfill path for derived or reconstructed events instead of RecordProvenanceEventInTx","Normalize your source strings and compare against ReservedProvSource before recording","On import, rewrite reserved sources from the source export to your own namespace"],"exampleFix":"// before\nev.Source = \"ingest-backfill\" // reserved\n_ = issueops.RecordProvenanceEventInTx(ctx, tx, ev)\n// after\nev.Source = \"import:mytool\"\nif strings.EqualFold(strings.TrimSpace(ev.Source), issueops.ReservedProvSource) {\n\treturn fmt.Errorf(\"source %q is reserved; pick another\", ev.Source)\n}\nreturn issueops.RecordProvenanceEventInTx(ctx, tx, ev)","handlingStrategy":"validation","validationCode":"if strings.EqualFold(strings.TrimSpace(ev.Source), issueops.ReservedProvSource) {\n\treturn fmt.Errorf(\"source %q is reserved for ingest backfill\", ev.Source)\n}\nreturn issueops.ValidateProvenanceEvent(ev)","typeGuard":"func isReservedSource(s string) bool { return strings.EqualFold(strings.TrimSpace(s), issueops.ReservedProvSource) }","tryCatchPattern":"if err := issueops.RecordProvenanceEventInTx(ctx, tx, ev); err != nil {\n\tif strings.Contains(err.Error(), \"is reserved\") {\n\t\treturn fmt.Errorf(\"rewrite source %q to your own namespace before recording\", ev.Source)\n\t}\n\treturn err\n}","preventionTips":["Namespace your sources (e.g. \"import:mytool\") instead of reusing system values","Compare imports against issueops.ReservedProvSource (case-insensitively) and rewrite on conflict","Route derived/reconstructed events through the ingest backfill path, not the record path","Treat the reserved source as an integrity marker, never as a label for your own events"],"tags":["go","validation","provenance","reserved-value"],"backgroundTag":"provenance-validation-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}