{"record":{"id":"b616c2ea4ebae29f","repo":"gastownhall/beads","slug":"assignee-must-not-be-empty-for-an-assignee-scoped","errorCode":null,"errorMessage":"assignee must not be empty for an assignee-scoped summary","messagePattern":"assignee must not be empty for an assignee-scoped summary","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/workapi/stats.go","lineNumber":28,"sourceCode":"\n// The shared half of issueops.StatsReporter: the assignee-scoped question,\n// which every implementation answers by asking storage two questions and\n// folding the answers, rather than by running one aggregate.\n//\n// The workspace-wide question needs nothing here — it is a single seam call on\n// every backend. This file exists because the assignee-scoped answer is\n// ASSEMBLED, and assembling it twice is how the two front doors would come to\n// disagree about what \"your work\" means.\n\n// ValidateStatsAssignee resolves the actor an assignee-scoped summary answers\n// for, refusing an empty or whitespace-only one with ErrValidation.\n//\n// It returns the value UNCHANGED when it accepts: an assignee is an opaque\n// identifier this layer has no vocabulary for, so trimming it would silently\n// answer for a different actor than the caller named.\nfunc ValidateStatsAssignee(assignee string) (string, error) {\n\tif strings.TrimSpace(assignee) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"assignee must not be empty for an assignee-scoped summary%.0w\", issueops.ErrValidation)\n\t}\n\treturn assignee, nil\n}\n\n// BuildStatsAssigneeIssueFilter is the predicate that selects one actor's rows\n// for the fold below.\n//\n// It carries the assignee and NOTHING else, which is a decision and not an\n// omission: no status restriction (the fold tallies every status, including\n// closed), no limit (a capped scan would silently under-report a busy actor's\n// total), and no wisp suppression — the search seam merges the ephemeral tier\n// unless told not to.\nfunc BuildStatsAssigneeIssueFilter(assignee string) types.IssueFilter {\n\treturn types.IssueFilter{Assignee: &assignee}\n}\n\n// BuildStatsAssigneeWorkFilter is the ready-work predicate for the same actor.\n// It is a second filter type because ready work is a different question with","sourceCodeStart":10,"sourceCodeEnd":46,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workapi/stats.go#L10-L46","documentation":"ValidateStatsAssignee refuses an empty or whitespace-only assignee when building an assignee-scoped summary. An assignee is an opaque identifier this layer has no vocabulary for, so it is returned UNCHANGED when accepted — trimming would silently answer for a different actor than the caller named. There is no default assignee to fall back to.","triggerScenarios":"Calling workapi.ValidateStatsAssignee(\"\") or ValidateStatsAssignee(\"   \") — directly or via a stats role that scopes a summary to one actor.","commonSituations":"A CLI handler passed an unset --assignee flag value straight through; a config/env lookup for the current user returned empty; a caller assumed an empty string meant 'all assignees' rather than omitting the scope.","solutions":["Pass a non-empty assignee identifier, or skip the assignee-scoped summary entirely when no assignee was requested","Guard at the call site: only build an assignee-scoped summary when an assignee was actually provided","Compare against errors.Is(err, issueops.ErrValidation) and prompt the user for the assignee"],"exampleFix":"// before\nassignee, err := workapi.ValidateStatsAssignee(flagAssignee) // flagAssignee == \"\"\n// after\nif flagAssignee != \"\" {\n    assignee, err = workapi.ValidateStatsAssignee(flagAssignee)\n} else {\n    // build an unscoped summary instead\n}","handlingStrategy":"validation","validationCode":"if strings.TrimSpace(assignee) == \"\" {\n    return fmt.Errorf(\"assignee-scoped summary requires a non-empty assignee\")\n}","typeGuard":"func isScopedSummary(assignee string) bool {\n    return strings.TrimSpace(assignee) != \"\"\n}","tryCatchPattern":"a, err := workapi.ValidateStatsAssignee(assignee)\nif err != nil {\n    if errors.Is(err, issueops.ErrValidation) {\n        return fmt.Errorf(\"assignee required: %w\", err)\n    }\n    return err\n}","preventionTips":["Only request an assignee-scoped summary when an assignee was actually provided; use the unscoped path otherwise","Treat assignee as an opaque string — never trim, default, or normalize it yourself","Check flag/env sources for emptiness before passing them through"],"tags":["go","validation","stats","missing-assignee"],"backgroundTag":"missing-required-field","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}