gastownhall/beads · error

unexpected node type: %T

Error message

unexpected node type: %T

What it means

buildFilter encountered an AST node type it has no case for; only ComparisonNode, AndNode, NotNode and the optimizable OrNode are supported. This is an internal invariant violation — it means the parser produced (or a caller constructed) a node kind the filter builder doesn't understand, and the %T verb names the offending type.

Source

Thrown at internal/query/evaluator.go:152

	case *ComparisonNode:
		return e.applyComparison(n, filter)
	case *AndNode:
		if err := e.buildFilter(n.Left, filter); err != nil {
			return err
		}
		return e.buildFilter(n.Right, filter)
	case *NotNode:
		return e.applyNot(n, filter)
	case *OrNode:
		// Only reached for LabelsAny optimization
		labels := e.collectOrLabels(n)
		if labels != nil {
			filter.LabelsAny = append(filter.LabelsAny, labels...)
			return nil
		}
		return fmt.Errorf("OR not supported for this field combination")
	default:
		return fmt.Errorf("unexpected node type: %T", node)
	}
}

// applyComparison applies a comparison to the filter.
func (e *Evaluator) applyComparison(comp *ComparisonNode, filter *types.IssueFilter) error {
	switch comp.Field {
	case "status":
		return e.applyStatusFilter(comp, filter)
	case "priority":
		return e.applyPriorityFilter(comp, filter)
	case "type":
		return e.applyTypeFilter(comp, filter)
	case "assignee":
		return e.applyAssigneeFilter(comp, filter)
	case "owner":
		return e.applyOwnerFilter(comp, filter)
	case "label", "labels":
		return e.applyLabelFilter(comp, filter)

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the %T type in the message and remove or rewrite that node kind before calling Evaluate
  2. Restrict programmatically built ASTs to ComparisonNode, AndNode, NotNode and label-only OrNodes
  3. Update the evaluator/parser together — ensure both sides come from the same library version
  4. If you need the construct, extend buildFilter with a case rather than passing it through

Example fix

// before
node := &CustomGroupNode{Children: children}
evaluator.Evaluate(node)
// after
node := &AndNode{Left: left, Right: right} // supported node type
evaluator.Evaluate(node)
Defensive patterns

Strategy: type-guard

Validate before calling

// Ensure the AST contains only node kinds buildFilter understands
func filterSafe(n query.Node) bool {
    switch t := n.(type) {
    case *query.ComparisonNode, *query.AndNode, *query.NotNode:
        return true
    case *query.OrNode:
        return filterSafe(t.Left) && filterSafe(t.Right)
    default:
        return false
    }
}

Type guard

func isFilterNode(n query.Node) bool {
    switch n.(type) {
    case *query.ComparisonNode, *query.AndNode, *query.NotNode, *query.OrNode:
        return true
    default:
        return false
    }
}

Try / catch

if err := evaluator.Evaluate(ast); err != nil {
    if strings.Contains(err.Error(), "unexpected node type") {
        return fmt.Errorf("internal: query AST contains unsupported node: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Passing an AST containing node types outside the filter-supported subset (e.g. grouping/nesting node kinds, custom Node implementations) into Evaluate/buildFilter; a parser bug emitting a new node type after a grammar change.

Common situations: Constructing query ASTs programmatically with unsupported node types; version skew where a newer parser emits nodes an older evaluator doesn't handle; third-party code implementing the Node interface.

Related errors


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