gastownhall/beads · error · storage.ErrValidation

%w: status value of type %T is neither a string nor a types.

Error message

%w: status value of type %T is neither a string nor a types.Status

What it means

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.

Source

Thrown at internal/storage/issueops/update.go:306

// a false. Both write funnels ask this question to decide whether close policy
// applies, so answering "no crossing" for a value nobody can read would let an
// in-process caller that got the transport wrong land status='closed' with the
// policy gate skipped — on an issue with open children, no less. Refusing here
// gives a mis-typed status the same fail-loud handling the mis-typed override
// key already gets (see PopForceClosePolicy).
func CrossesIntoDoneCategoryInTx(ctx context.Context, tx DBTX, oldStatus types.Status, updates map[string]interface{}) (bool, error) {
	rawStatus, hasStatus := updates["status"]
	if !hasStatus {
		return false, nil
	}
	var newStatus types.Status
	switch value := rawStatus.(type) {
	case string:
		newStatus = types.Status(value)
	case types.Status:
		newStatus = value
	default:
		return false, fmt.Errorf("%w: status value of type %T is neither a string nor a types.Status", storage.ErrValidation, rawStatus)
	}

	newCategory, err := ReopenCategoryInTx(ctx, tx, newStatus)
	if err != nil {
		return false, err
	}
	if newCategory != types.CategoryDone {
		return false, nil
	}
	oldCategory, err := ReopenCategoryInTx(ctx, tx, oldStatus)
	if err != nil {
		return false, err
	}
	return oldCategory != types.CategoryDone, nil
}

// UpdateResult holds the result of an UpdateIssueInTx call.
type UpdateResult struct {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Cast status to string or types.Status before placing it in the updates map.
  2. If the value may be absent, only set updates["status"] when it is a valid non-empty string.
  3. Validate status against types.Status values before calling update.

Example fix

// before
updates["status"] = someInterfaceValue
// after
s, ok := someInterfaceValue.(string)
if !ok {
    return fmt.Errorf("bad status type %T", someInterfaceValue)
}
updates["status"] = types.Status(s)
Defensive patterns

Strategy: type-guard

Validate before calling

v, ok := updates["status"]
if ok {
    if _, isStr := v.(string); !isStr {
        if _, isStatus := v.(types.Status); !isStatus {
            return fmt.Errorf("status must be string or types.Status, got %T", v)
        }
    }
}

Type guard

func validStatusValue(v interface{}) (types.Status, bool) {
    switch s := v.(type) {
    case string:
        return types.Status(s), true
    case types.Status:
        return s, true
    }
    return "", false
}

Try / catch

if _, err := storage.UpdateIssue(ctx, id, updates, actor); err != nil {
    if errors.Is(err, storage.ErrValidation) && strings.Contains(err.Error(), "neither a string nor a types.Status") {
        return fmt.Errorf("bad status in update map: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: 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.

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

Related errors


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