{"record":{"id":"155a0557fcf2bf95","repo":"gastownhall/beads","slug":"mixed-regular-wisp-createissues-batch-cannot-inclu","errorCode":null,"errorMessage":"mixed regular/wisp CreateIssues batch cannot include cross-bucket dependency %s -> %s; create the issues first, then add the in-batch dependency after both issues exist%.0w","messagePattern":"mixed regular/wisp CreateIssues batch cannot include cross-bucket dependency (.+?) -> (.+?); create the issues first, then add the in-batch dependency after both issues exist%\\.0w","errorType":"validation","errorClass":"publicops.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/create_batch.go","lineNumber":193,"sourceCode":"\t// counter bookkeeping here.\n\ttables := ChangedTables{}\n\ttables.Merge(CreateIssuesDirtyTables(ctx, issues, created))\n\tresult := publicops.CreateBatchResult{Issues: make([]*types.Issue, len(issues))}\n\tfor i, issue := range issues {\n\t\thydrated, err := HydrateIssueOperationResult(ctx, tx, issue.ID, false)\n\t\tif err != nil {\n\t\t\treturn publicops.CreateBatchResult{}, nil, err\n\t\t}\n\t\tresult.Issues[i] = hydrated\n\t}\n\treturn result, tables, nil\n}\n\n// CrossPlaneBatchEdgeError is the refusal the store-backed body raises from\n// filterCreateIssuesMixedBucketDependencies, spelled here so the unit-of-work\n// body can raise the identical one before it starts writing.\nfunc CrossPlaneBatchEdgeError(sourceID, targetID string) error {\n\treturn fmt.Errorf(\"mixed regular/wisp CreateIssues batch cannot include cross-bucket dependency %s -> %s; create the issues first, then add the in-batch dependency after both issues exist%.0w\",\n\t\tsourceID, targetID, publicops.ErrValidation)\n}\n\n// ValidateCreateBatchPlanes refuses a batch whose items would write an edge\n// BETWEEN the durable and ephemeral planes, which BatchCreator's contract says\n// \"cannot be written by the batch that creates both of its ends\".\n//\n// THE STORE BODY GETS THIS FOR FREE and the unit-of-work body does not. The\n// store body assigns every id first and then hands the whole slice to\n// CreateIssuesInTxWithResult, which sees the batch as a set and refuses. The\n// unit-of-work body creates item by item, so by the time an edge is written\n// its target is an ordinary existing row and the domain layer writes the\n// cross-plane edge happily — the identical request was refused whole by two\n// backends and landed in full on the third.\n//\n// Only an EXPLICIT id can be an in-batch target: a caller cannot name an id the\n// batch has not minted yet, so the ids known here are the only ones an edge can\n// reach inside the batch. infraTypes promotes the same types both bodies","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/create_batch.go#L175-L211","documentation":"CrossPlaneBatchEdgeError refuses a CreateIssues batch that mixes regular (durable) and wisp (ephemeral) issues while also declaring a dependency whose source and target land in different buckets. Such a cross-plane edge cannot be written by the batch that creates both ends; it must be added after both issues exist. Wraps publicops.ErrValidation.","triggerScenarios":"A single CreateIssues batch containing e.g. a regular issue A depending on wisp issue B (or vice versa) via item Dependencies, where the edge crosses the durable/ephemeral plane boundary.","commonSituations":"Bulk-importing mixed-priority work where a long-lived blocker references a transient wisp; tools generating one batch from a graph that does not distinguish wisps from regular issues.","solutions":["Split the batch: create both issues first (possibly in separate batches per plane), then issue a second dependency-add call for the cross-bucket edge","Convert one endpoint so both ends of the dependency live in the same bucket (both regular or both wisp)","Pre-screen items: for each dependency, check plane(item.Issue) == plane(dep.Target) before submitting the batch"],"exampleFix":"// before\nbatch.Dependencies = []publicops.CreateDependency{{IssueID: regularID, DependsOnID: wispID}}\n// after\nExecuteCreateBatch(ctx, batchWithoutCrossEdge)\nAddDependency(ctx, regularID, wispID) // after both exist","handlingStrategy":"validation","validationCode":"func isWisp(i *types.Issue) bool { /* wisp predicate */ }\nfor _, item := range req.Items {\n    plane := map[string]bool{}\n    for _, d := range item.Dependencies {\n        if plane[d.IssueID] != plane[d.DependsOnID] {\n            return fmt.Errorf(\"cross-plane edge %s -> %s\", d.IssueID, d.DependsOnID)\n        }\n    }\n}","typeGuard":null,"tryCatchPattern":"if err := ExecuteCreateBatch(ctx, req); errors.Is(err, publicops.ErrValidation) && strings.Contains(err.Error(), \"cross-bucket dependency\") {\n    // split batch and add edges afterwards\n}","preventionTips":["Never declare dependencies across regular/wisp items in one batch","Group imports per plane before batching","Add cross-plane edges in a follow-up dependency call after both creates succeed"],"tags":["validation","batch-create","dependency","wisp"],"backgroundTag":"cross-plane-dependency","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}