{"record":{"id":"8d0a842a43e04ba8","repo":"gastownhall/beads","slug":"cli-version-must-not-be-empty","errorCode":null,"errorMessage":"cli version must not be empty","messagePattern":"cli version must not be empty","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/workapi/versionreconcile.go","lineNumber":60,"sourceCode":"\t// together: a workspace catching up to a mark a newer binary already left\n\t// moves the marker to the mark and leaves the mark where it is.\n\tRecordHighWaterMark bool\n}\n\n// ValidateReconcileVersion checks the version a caller wants to record, and\n// returns it unchanged.\n//\n// It is separate from the planner so a body can refuse BEFORE it reads anything\n// or opens a unit of work. This runs on every startup, so a refusal that costs\n// a round trip is a refusal that costs every command.\n//\n// The only rule at this end is that a version has to name something. Recording\n// \"\" over a real marker would silently take the downgrade guard down with it,\n// and the empty marker it left behind is indistinguishable from a workspace\n// nothing has ever reconciled.\nfunc ValidateReconcileVersion(cliVersion string) (string, error) {\n\tif strings.TrimSpace(cliVersion) == \"\" {\n\t\treturn \"\", fmt.Errorf(\"%w: cli version must not be empty\", issueops.ErrValidation)\n\t}\n\treturn cliVersion, nil\n}\n\n// PlanVersionReconcile decides what recording cliVersion should do to a\n// workspace whose markers currently read recorded and highWaterMark.\n//\n// The order of the checks is the contract, not an implementation detail. An\n// exact match short-circuits first, so the steady-state path — every startup\n// that is not the first one after an upgrade — decides without consulting the\n// mark at all. Both downgrade guards then run BEFORE any write is planned, so\n// a refusal is a decision rather than a write that is undone.\nfunc PlanVersionReconcile(cliVersion, recorded, highWaterMark string) (VersionReconcilePlan, error) {\n\tif _, err := ValidateReconcileVersion(cliVersion); err != nil {\n\t\treturn VersionReconcilePlan{}, err\n\t}\n\n\tif recorded == cliVersion {","sourceCodeStart":42,"sourceCodeEnd":78,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/workapi/versionreconcile.go#L42-L78","documentation":"ValidateReconcileVersion refuses an empty or whitespace-only cliVersion. Recording \"\" over a real marker would silently break the downgrade guard, and the empty marker it leaves behind is indistinguishable from a workspace nothing has ever reconciled. The only rule at this end is that a version has to name something.","triggerScenarios":"Calling workapi.ValidateReconcileVersion(\"\") directly, or calling PlanVersionReconcile, which invokes it, with a version string that is empty or whitespace.","commonSituations":"A build-time ldflags version variable was not injected (binary built without the version flag); a caller read the version from an unset env/config value; a test harness forgot to set the version before planning the reconcile.","solutions":["Set cliVersion to the actual binary/build version string before calling PlanVersionReconcile","Fix the build pipeline so the version ldflag is injected (e.g. -X main.version=...)","Handle errors.Is(err, issueops.ErrValidation) and fail fast with 'cli version must not be empty'"],"exampleFix":"// before\nplan, err := workapi.PlanVersionReconcile(version) // version == \"\"\n// after\nif strings.TrimSpace(version) == \"\" {\n    return fmt.Errorf(\"refusing reconcile: binary built without a version\")\n}\nplan, err := workapi.PlanVersionReconcile(version)","handlingStrategy":"validation","validationCode":"if strings.TrimSpace(cliVersion) == \"\" {\n    return fmt.Errorf(\"cli version must not be empty before reconcile\")\n}","typeGuard":"func hasVersion(v string) bool {\n    return strings.TrimSpace(v) != \"\"\n}","tryCatchPattern":"plan, err := workapi.PlanVersionReconcile(cliVersion)\nif err != nil {\n    if errors.Is(err, issueops.ErrValidation) {\n        return fmt.Errorf(\"cannot reconcile: %w\", err)\n    }\n    return err\n}","preventionTips":["Inject the version at build time via ldflags and fail the build if it is empty","Never pass env/config-sourced version strings without an emptiness check","Fail fast at startup when the binary reports an empty version, before touching workspace markers"],"tags":["go","validation","versioning","reconcile"],"backgroundTag":"missing-required-field","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}