{"record":{"id":"ed4820d88a6a4f41","repo":"gastownhall/beads","slug":"a-durable-sweep-requires-a-closed-before-cutoff-or","errorCode":null,"errorMessage":"a durable sweep requires a closed-before cutoff or an id pattern; pass the pattern \"*\" to sweep every closed issue deliberately","messagePattern":"a durable sweep requires a closed-before cutoff or an id pattern; pass the pattern \"\\*\" to sweep every closed issue deliberately","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/workapi/sweep.go","lineNumber":51,"sourceCode":"\tcase \"\":\n\t\treturn fmt.Errorf(\"%w: sweep requires a tier (%q or %q), and has no default\",\n\t\t\tissueops.ErrValidation, issueops.SweepEphemeral, issueops.SweepDurable)\n\tdefault:\n\t\treturn fmt.Errorf(\"%w: %q is not a sweep tier; use %q or %q\",\n\t\t\tissueops.ErrValidation, in.Tier, issueops.SweepEphemeral, issueops.SweepDurable)\n\t}\n\tif in.IDPattern != \"\" {\n\t\t// filepath.Match reports a malformed pattern on any subject, so one\n\t\t// probe against the empty string classifies the pattern itself. The\n\t\t// front doors used to discard this error, which turned `--pattern '['`\n\t\t// into \"nothing matched\" on a command whose job is to delete matches.\n\t\tif _, err := filepath.Match(in.IDPattern, \"\"); err != nil {\n\t\t\treturn fmt.Errorf(\"%w: --pattern %q is not a valid glob: %v\",\n\t\t\t\tissueops.ErrValidation, in.IDPattern, err)\n\t\t}\n\t}\n\tif in.Tier == issueops.SweepDurable && in.ClosedBefore == nil && in.IDPattern == \"\" {\n\t\treturn fmt.Errorf(\"%w: a durable sweep requires a closed-before cutoff or an id pattern; \"+\n\t\t\t\"pass the pattern \\\"*\\\" to sweep every closed issue deliberately\",\n\t\t\tissueops.ErrValidation)\n\t}\n\treturn nil\n}\n\n// BuildSweepCandidateFilter turns a sweep request into the storage-level\n// filter that selects its CANDIDATES: the closed rows of one tier, bounded by\n// the cutoff.\n//\n// The pattern is deliberately NOT in the filter. Globs are matched in Go\n// (MatchesSweepPattern) because a LIKE translation would silently disagree with\n// filepath.Match on `[...]` and on the escape rules — a disagreement that, on\n// this operation, decides which rows are deleted.\n//\n// Call ValidateSweepRequest first; this builder assumes a validated request\n// and does not re-refuse one.\nfunc BuildSweepCandidateFilter(in issueops.SweepRequest) types.IssueFilter {","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workapi/sweep.go#L33-L69","documentation":"ValidateSweepRequest refuses a durable-tier sweep that has neither a ClosedBefore cutoff nor an IDPattern. This is a safety invariant (deliberately enforced in the shared validator, not the CLI): a durable sweep with no filter would delete every closed durable issue, so the caller must state a cutoff or an explicit pattern — \"*\" acknowledges the intent to sweep everything closed.","triggerScenarios":"Calling a Sweeper with issueops.SweepRequest{Tier: issueops.SweepDurable, ClosedBefore: nil, IDPattern: \"\"} — a durable sweep with no scope at all.","commonSituations":"A caller wanted 'prune old closed issues' but forgot to pass --closed-before; a wrapper built the request programmatically and left both filters unset; a user expected the ephemeral-tier behavior (no filter required) to apply to durable sweeps.","solutions":["Pass ClosedBefore (a *time.Time cutoff) so only issues closed before it are swept","Pass an IDPattern to scope which closed issues are eligible; use \"*\" to deliberately sweep every closed issue","Switch to issueops.SweepEphemeral if the unfiltered intent was really about ephemeral wisps","Handle errors.Is(err, issueops.ErrValidation) and prompt for a cutoff or pattern"],"exampleFix":"// before\nreq := issueops.SweepRequest{Tier: issueops.SweepDurable} // no filter\n// after\ncutoff := time.Now().AddDate(0, -6, 0)\nreq := issueops.SweepRequest{Tier: issueops.SweepDurable, ClosedBefore: &cutoff}\n// or, deliberately sweep all closed:\nreq := issueops.SweepRequest{Tier: issueops.SweepDurable, IDPattern: \"*\"}","handlingStrategy":"validation","validationCode":"if req.Tier == issueops.SweepDurable && req.ClosedBefore == nil && req.IDPattern == \"\" {\n    return fmt.Errorf(\"durable sweep requires a closed-before cutoff or an id pattern (use \\\"*\\\" to sweep all)\")\n}","typeGuard":"func durableSweepIsScoped(req issueops.SweepRequest) bool {\n    return req.Tier != issueops.SweepDurable || req.ClosedBefore != nil || req.IDPattern != \"\"\n}","tryCatchPattern":"if err := sweeper.Sweep(ctx, req); err != nil {\n    if errors.Is(err, issueops.ErrValidation) {\n        return fmt.Errorf(\"unscoped durable sweep refused: %w\", err)\n    }\n    return err\n}","preventionTips":["Always pass a cutoff or pattern for durable sweeps; make it a required flag in wrappers","Use ClosedBefore with a conservative (recent) cutoff for routine pruning","Reserve IDPattern \"*\" for the explicit, deliberate sweep-everything case","Add integration tests asserting an unscoped durable sweep is refused"],"tags":["go","validation","sweep","safety-guard","missing-filter"],"backgroundTag":"missing-required-field","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}