{"record":{"id":"8cf1e444552a2986","repo":"gastownhall/beads","slug":"sweep-requires-a-tier-q-or-q-and-has-no-defau","errorCode":null,"errorMessage":"sweep requires a tier (%q or %q), and has no default","messagePattern":"sweep requires a tier \\(%q or %q\\), and has no default","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/workapi/sweep.go","lineNumber":34,"sourceCode":"// Every implementation of the role runs these, so `bd purge` and `bd prune`\n// have one definition rather than one per backend.\n//\n// What is NOT here is the sweep itself. Selecting rows and deleting them needs\n// one transaction (issueops.Sweeper.Sweep), which no interface above a store\n// publishes; the bodies live in internal/storage/issueops/sweep.go and in the\n// unit-of-work provider.\n\n// ValidateSweepRequest applies the request rules every Sweeper implementation\n// shares, before anything is read.\n//\n// The require-a-filter refusal for the durable tier is a safety invariant, so\n// it lives HERE rather than in a CLI handler: a second front door inherits it\n// by calling the role. See issueops.SweepRequest.\nfunc ValidateSweepRequest(in issueops.SweepRequest) error {\n\tswitch in.Tier {\n\tcase issueops.SweepEphemeral, issueops.SweepDurable:\n\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\",","sourceCodeStart":16,"sourceCodeEnd":52,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workapi/sweep.go#L16-L52","documentation":"ValidateSweepRequest refuses a SweepRequest whose Tier is the empty string. A sweep must name its tier — SweepEphemeral or SweepDurable — and there is deliberately no default tier, because the tier decides which rows the sweep may delete. The safety-critical rules live in the shared validator so every front door inherits them.","triggerScenarios":"Calling a Sweeper implementation (which runs ValidateSweepRequest) with issueops.SweepRequest{Tier: \"\"}, regardless of pattern or cutoff values.","commonSituations":"A CLI handler did not map its purge/prune subcommand onto a tier value; a caller built the struct literally and left Tier unset; a config value for the tier was blank.","solutions":["Set Tier explicitly to issueops.SweepEphemeral or issueops.SweepDurable","Map the CLI subcommand (`bd purge` vs `bd prune`) onto the correct tier constant before building the request","Handle errors.Is(err, issueops.ErrValidation) and tell the user which tier values are accepted"],"exampleFix":"// before\nreq := issueops.SweepRequest{ClosedBefore: &cutoff}\n// after\nreq := issueops.SweepRequest{Tier: issueops.SweepDurable, ClosedBefore: &cutoff}","handlingStrategy":"validation","validationCode":"if req.Tier == \"\" {\n    return fmt.Errorf(\"sweep requires an explicit tier (ephemeral or durable)\")\n}","typeGuard":"func hasTier(req issueops.SweepRequest) bool {\n    return req.Tier == issueops.SweepEphemeral || req.Tier == issueops.SweepDurable\n}","tryCatchPattern":"if err := sweeper.Sweep(ctx, req); err != nil {\n    if errors.Is(err, issueops.ErrValidation) {\n        return fmt.Errorf(\"invalid sweep request: %w\", err)\n    }\n    return err\n}","preventionTips":["Always set Tier from the exported constants, never from user strings directly","Map each CLI subcommand to exactly one tier constant","Never assume a default tier — the API deliberately has none"],"tags":["go","validation","sweep","missing-tier"],"backgroundTag":"missing-required-field","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}