gastownhall/beads · error

sweep requires a tier (%q or %q), and has no default

Error message

sweep requires a tier (%q or %q), and has no default

What it means

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.

Source

Thrown at internal/workapi/sweep.go:34

// Every implementation of the role runs these, so `bd purge` and `bd prune`
// have one definition rather than one per backend.
//
// What is NOT here is the sweep itself. Selecting rows and deleting them needs
// one transaction (issueops.Sweeper.Sweep), which no interface above a store
// publishes; the bodies live in internal/storage/issueops/sweep.go and in the
// unit-of-work provider.

// ValidateSweepRequest applies the request rules every Sweeper implementation
// shares, before anything is read.
//
// The require-a-filter refusal for the durable tier is a safety invariant, so
// it lives HERE rather than in a CLI handler: a second front door inherits it
// by calling the role. See issueops.SweepRequest.
func ValidateSweepRequest(in issueops.SweepRequest) error {
	switch in.Tier {
	case issueops.SweepEphemeral, issueops.SweepDurable:
	case "":
		return fmt.Errorf("%w: sweep requires a tier (%q or %q), and has no default",
			issueops.ErrValidation, issueops.SweepEphemeral, issueops.SweepDurable)
	default:
		return fmt.Errorf("%w: %q is not a sweep tier; use %q or %q",
			issueops.ErrValidation, in.Tier, issueops.SweepEphemeral, issueops.SweepDurable)
	}
	if in.IDPattern != "" {
		// filepath.Match reports a malformed pattern on any subject, so one
		// probe against the empty string classifies the pattern itself. The
		// front doors used to discard this error, which turned `--pattern '['`
		// into "nothing matched" on a command whose job is to delete matches.
		if _, err := filepath.Match(in.IDPattern, ""); err != nil {
			return fmt.Errorf("%w: --pattern %q is not a valid glob: %v",
				issueops.ErrValidation, in.IDPattern, err)
		}
	}
	if in.Tier == issueops.SweepDurable && in.ClosedBefore == nil && in.IDPattern == "" {
		return fmt.Errorf("%w: a durable sweep requires a closed-before cutoff or an id pattern; "+
			"pass the pattern \"*\" to sweep every closed issue deliberately",

View on GitHub (pinned to 71377f2769)

Solutions

  1. Set Tier explicitly to issueops.SweepEphemeral or issueops.SweepDurable
  2. Map the CLI subcommand (`bd purge` vs `bd prune`) onto the correct tier constant before building the request
  3. Handle errors.Is(err, issueops.ErrValidation) and tell the user which tier values are accepted

Example fix

// before
req := issueops.SweepRequest{ClosedBefore: &cutoff}
// after
req := issueops.SweepRequest{Tier: issueops.SweepDurable, ClosedBefore: &cutoff}
Defensive patterns

Strategy: validation

Validate before calling

if req.Tier == "" {
    return fmt.Errorf("sweep requires an explicit tier (ephemeral or durable)")
}

Type guard

func hasTier(req issueops.SweepRequest) bool {
    return req.Tier == issueops.SweepEphemeral || req.Tier == issueops.SweepDurable
}

Try / catch

if err := sweeper.Sweep(ctx, req); err != nil {
    if errors.Is(err, issueops.ErrValidation) {
        return fmt.Errorf("invalid sweep request: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Calling a Sweeper implementation (which runs ValidateSweepRequest) with issueops.SweepRequest{Tier: ""}, regardless of pattern or cutoff values.

Common situations: 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.

Understand the failure class

Background: "Missing required field" and "field is required" errors: why libraries reject payloads that omit mandatory fields — this error's family across 20 libraries.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/8cf1e444552a2986. Report an issue: GitHub.