{"record":{"id":"2178e486f77e23fb","repo":"gastownhall/beads","slug":"w-status-value-of-type-t-is-neither-a-string-no","errorCode":null,"errorMessage":"%w: status value of type %T is neither a string nor a types.Status","messagePattern":"%w: status value of type %T is neither a string nor a types\\.Status","errorType":"validation","errorClass":"storage.ErrValidation","httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/update.go","lineNumber":306,"sourceCode":"// a false. Both write funnels ask this question to decide whether close policy\n// applies, so answering \"no crossing\" for a value nobody can read would let an\n// in-process caller that got the transport wrong land status='closed' with the\n// policy gate skipped — on an issue with open children, no less. Refusing here\n// gives a mis-typed status the same fail-loud handling the mis-typed override\n// key already gets (see PopForceClosePolicy).\nfunc CrossesIntoDoneCategoryInTx(ctx context.Context, tx DBTX, oldStatus types.Status, updates map[string]interface{}) (bool, error) {\n\trawStatus, hasStatus := updates[\"status\"]\n\tif !hasStatus {\n\t\treturn false, nil\n\t}\n\tvar newStatus types.Status\n\tswitch value := rawStatus.(type) {\n\tcase string:\n\t\tnewStatus = types.Status(value)\n\tcase types.Status:\n\t\tnewStatus = value\n\tdefault:\n\t\treturn false, fmt.Errorf(\"%w: status value of type %T is neither a string nor a types.Status\", storage.ErrValidation, rawStatus)\n\t}\n\n\tnewCategory, err := ReopenCategoryInTx(ctx, tx, newStatus)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tif newCategory != types.CategoryDone {\n\t\treturn false, nil\n\t}\n\toldCategory, err := ReopenCategoryInTx(ctx, tx, oldStatus)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\treturn oldCategory != types.CategoryDone, nil\n}\n\n// UpdateResult holds the result of an UpdateIssueInTx call.\ntype UpdateResult struct {","sourceCodeStart":288,"sourceCodeEnd":324,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/update.go#L288-L324","documentation":"CrossesIntoDoneCategoryInTx received a status value that is neither a string nor a types.Status, so it cannot determine the status's reopen category. The error wraps storage.ErrValidation and reports the offending Go type via %T.","triggerScenarios":"Passing a non-string updates[\"status\"] into updateIssueInTx — e.g. nil, an int, a fmt.Stringer, *types.Status pointer, or a custom type — from code constructing the update map programmatically.","commonSituations":"JSON unmarshaling into interface{} producing float64 or nil for status; reflection-driven updaters passing typed enums from other packages; a refactor changing types.Status that leaves pointers or wrapper types in the map.","solutions":["Cast status to string or types.Status before placing it in the updates map.","If the value may be absent, only set updates[\"status\"] when it is a valid non-empty string.","Validate status against types.Status values before calling update."],"exampleFix":"// before\nupdates[\"status\"] = someInterfaceValue\n// after\ns, ok := someInterfaceValue.(string)\nif !ok {\n    return fmt.Errorf(\"bad status type %T\", someInterfaceValue)\n}\nupdates[\"status\"] = types.Status(s)","handlingStrategy":"type-guard","validationCode":"v, ok := updates[\"status\"]\nif ok {\n    if _, isStr := v.(string); !isStr {\n        if _, isStatus := v.(types.Status); !isStatus {\n            return fmt.Errorf(\"status must be string or types.Status, got %T\", v)\n        }\n    }\n}","typeGuard":"func validStatusValue(v interface{}) (types.Status, bool) {\n    switch s := v.(type) {\n    case string:\n        return types.Status(s), true\n    case types.Status:\n        return s, true\n    }\n    return \"\", false\n}","tryCatchPattern":"if _, err := storage.UpdateIssue(ctx, id, updates, actor); err != nil {\n    if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), \"neither a string nor a types.Status\") {\n        return fmt.Errorf(\"bad status in update map: %w\", err)\n    }\n    return err\n}","preventionTips":["Build update maps with typed literals, not decoded interface{} values.","Coerce JSON-decoded values (float64/interface{}) to string before storing status.","Centralize update-map construction in one validated helper.","Add a unit test that every status key is string or types.Status."],"tags":["storage","validation","status","type-mismatch"],"backgroundTag":"invalid-status-type","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}