{"record":{"id":"3739566955a1f709","repo":"gastownhall/beads","slug":"provenance-issue-id-is-required","errorCode":null,"errorMessage":"provenance: issue id is required","messagePattern":"provenance: issue id is required","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/provenance.go","lineNumber":52,"sourceCode":"\t\"work-id\":    {},\n\t\"transcript\": {},\n\t\"branch\":     {},\n}\n\n// ReservedProvSource is reserved for derived/reconstructed events so a consumer's\n// read-first honesty filter can exclude backfilled rows. The record path rejects\n// it (case-insensitively): real producers must name their own source.\nconst ReservedProvSource = \"ingest-backfill\"\n\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\" {","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/provenance.go#L34-L70","documentation":"ValidateProvenanceEvent enforces the structural rules for a types.ProvenanceEvent before it is stored. A ProvenanceEvent with an empty or whitespace-only IssueID is rejected because every provenance event must be attributed to an issue. The same validation runs in RecordProvenanceEventInTx and is exported so the CLI can fail early with identical rules.","triggerScenarios":"Recording a provenance event via RecordProvenanceEventInTx (or pre-validating via ValidateProvenanceEvent) with ev.IssueID == \"\" or only whitespace (e.g. \"   \").","commonSituations":"Building events programmatically where the issue ID variable was never populated; parsing CLI output where an empty field became an empty string; calling RecordProvenanceEvent before the issue ID has been assigned (e.g. before create returns the ID).","solutions":["Populate ev.IssueID with the real issue ID before validation/recording","Call bd create / the create API first and use its returned ID; never record provenance for an un-created issue","Trim and check the field in your own code before constructing the event","For batch imports, skip or queue events whose issue ID is unknown and log them instead of failing the batch"],"exampleFix":"// before\n_ = issueops.RecordProvenanceEventInTx(ctx, tx, types.ProvenanceEvent{Kind: types.ProvClaim, Source: \"agent\"})\n// after\nev := types.ProvenanceEvent{IssueID: issue.ID, Kind: types.ProvClaim, Source: \"agent\"}\nif err := issueops.ValidateProvenanceEvent(ev); err != nil {\n\treturn fmt.Errorf(\"invalid provenance event: %w\", err)\n}\nreturn issueops.RecordProvenanceEventInTx(ctx, tx, ev)","handlingStrategy":"validation","validationCode":"func validProvEvent(ev types.ProvenanceEvent) error {\n\tif strings.TrimSpace(ev.IssueID) == \"\" { return errors.New(\"provenance: issue id is required\") }\n\treturn issueops.ValidateProvenanceEvent(ev)\n}","typeGuard":"func hasIssueID(ev types.ProvenanceEvent) bool { return strings.TrimSpace(ev.IssueID) != \"\" }","tryCatchPattern":"if err := issueops.RecordProvenanceEventInTx(ctx, tx, ev); err != nil {\n\tif strings.Contains(err.Error(), \"issue id is required\") {\n\t\treturn fmt.Errorf(\"cannot record provenance: event not bound to an issue (id=%q)\", ev.IssueID)\n\t}\n\treturn err\n}","preventionTips":["Construct events only after the issue ID is known (post-create)","Run ValidateProvenanceEvent before opening any transaction","Use a constructor helper that requires issueID so it cannot be omitted","Trim IDs — whitespace-only values fail the same check"],"tags":["go","validation","provenance"],"backgroundTag":"provenance-validation-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}