Test new approach with AST (AI assisted)
diff --git a/pkg/ast/ast.go b/pkg/ast/ast.go
new file mode 100644
index 0000000..aac7016
--- /dev/null
+++ b/pkg/ast/ast.go
@@ -0,0 +1,73 @@
+package ast
+
+// NodeType represents the type of a node in the AST
+type NodeType string
+
+const (
+	TokenNode     NodeType = "token"
+	TermGroupNode NodeType = "termGroup"
+	TermNode      NodeType = "term"
+)
+
+// RelationType represents the type of relation between nodes
+type RelationType string
+
+const (
+	AndRelation RelationType = "and"
+	OrRelation  RelationType = "or"
+)
+
+// MatchType represents the type of match operation
+type MatchType string
+
+const (
+	MatchEqual    MatchType = "eq"
+	MatchNotEqual MatchType = "ne"
+)
+
+// Node represents a node in the AST
+type Node interface {
+	Type() NodeType
+}
+
+// Token represents a token node in the query
+type Token struct {
+	Wrap Node `json:"wrap"`
+}
+
+func (t *Token) Type() NodeType {
+	return TokenNode
+}
+
+// TermGroup represents a group of terms with a relation
+type TermGroup struct {
+	Operands []Node       `json:"operands"`
+	Relation RelationType `json:"relation"`
+}
+
+func (tg *TermGroup) Type() NodeType {
+	return TermGroupNode
+}
+
+// Term represents a terminal node with matching criteria
+type Term struct {
+	Foundry string    `json:"foundry"`
+	Key     string    `json:"key"`
+	Layer   string    `json:"layer"`
+	Match   MatchType `json:"match"`
+	Value   string    `json:"value,omitempty"`
+}
+
+func (t *Term) Type() NodeType {
+	return TermNode
+}
+
+// Pattern represents a pattern to match in the AST
+type Pattern struct {
+	Root Node
+}
+
+// Replacement represents a replacement pattern
+type Replacement struct {
+	Root Node
+}
diff --git a/pkg/ast/ast_test.go b/pkg/ast/ast_test.go
new file mode 100644
index 0000000..1a01d0b
--- /dev/null
+++ b/pkg/ast/ast_test.go
@@ -0,0 +1,190 @@
+package ast
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNodeTypes(t *testing.T) {
+	tests := []struct {
+		name     string
+		node     Node
+		expected NodeType
+	}{
+		{
+			name:     "Token node returns correct type",
+			node:     &Token{Wrap: &Term{}},
+			expected: TokenNode,
+		},
+		{
+			name: "TermGroup node returns correct type",
+			node: &TermGroup{
+				Operands: []Node{&Term{}},
+				Relation: AndRelation,
+			},
+			expected: TermGroupNode,
+		},
+		{
+			name: "Term node returns correct type",
+			node: &Term{
+				Foundry: "opennlp",
+				Key:     "DET",
+				Layer:   "p",
+				Match:   MatchEqual,
+			},
+			expected: TermNode,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.expected, tt.node.Type())
+		})
+	}
+}
+
+func TestTermGroupConstruction(t *testing.T) {
+	term1 := &Term{
+		Foundry: "opennlp",
+		Key:     "DET",
+		Layer:   "p",
+		Match:   MatchEqual,
+	}
+
+	term2 := &Term{
+		Foundry: "opennlp",
+		Key:     "AdjType",
+		Layer:   "m",
+		Match:   MatchEqual,
+		Value:   "Pdt",
+	}
+
+	group := &TermGroup{
+		Operands: []Node{term1, term2},
+		Relation: AndRelation,
+	}
+
+	assert.Len(t, group.Operands, 2)
+	assert.Equal(t, AndRelation, group.Relation)
+	assert.Equal(t, TermGroupNode, group.Type())
+
+	// Test operands are correctly set
+	assert.Equal(t, term1, group.Operands[0])
+	assert.Equal(t, term2, group.Operands[1])
+}
+
+func TestTokenConstruction(t *testing.T) {
+	term := &Term{
+		Foundry: "opennlp",
+		Key:     "DET",
+		Layer:   "p",
+		Match:   MatchEqual,
+	}
+
+	token := &Token{Wrap: term}
+
+	assert.Equal(t, TokenNode, token.Type())
+	assert.Equal(t, term, token.Wrap)
+}
+
+func TestTermConstruction(t *testing.T) {
+	tests := []struct {
+		name     string
+		term     *Term
+		foundry  string
+		key      string
+		layer    string
+		match    MatchType
+		hasValue bool
+		value    string
+	}{
+		{
+			name: "Term without value",
+			term: &Term{
+				Foundry: "opennlp",
+				Key:     "DET",
+				Layer:   "p",
+				Match:   MatchEqual,
+			},
+			foundry:  "opennlp",
+			key:      "DET",
+			layer:    "p",
+			match:    MatchEqual,
+			hasValue: false,
+		},
+		{
+			name: "Term with value",
+			term: &Term{
+				Foundry: "opennlp",
+				Key:     "AdjType",
+				Layer:   "m",
+				Match:   MatchEqual,
+				Value:   "Pdt",
+			},
+			foundry:  "opennlp",
+			key:      "AdjType",
+			layer:    "m",
+			match:    MatchEqual,
+			hasValue: true,
+			value:    "Pdt",
+		},
+		{
+			name: "Term with not equal match",
+			term: &Term{
+				Foundry: "opennlp",
+				Key:     "DET",
+				Layer:   "p",
+				Match:   MatchNotEqual,
+			},
+			foundry:  "opennlp",
+			key:      "DET",
+			layer:    "p",
+			match:    MatchNotEqual,
+			hasValue: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, TermNode, tt.term.Type())
+			assert.Equal(t, tt.foundry, tt.term.Foundry)
+			assert.Equal(t, tt.key, tt.term.Key)
+			assert.Equal(t, tt.layer, tt.term.Layer)
+			assert.Equal(t, tt.match, tt.term.Match)
+			if tt.hasValue {
+				assert.Equal(t, tt.value, tt.term.Value)
+			} else {
+				assert.Empty(t, tt.term.Value)
+			}
+		})
+	}
+}
+
+func TestPatternAndReplacement(t *testing.T) {
+	// Create a simple pattern
+	patternTerm := &Term{
+		Foundry: "opennlp",
+		Key:     "DET",
+		Layer:   "p",
+		Match:   MatchEqual,
+	}
+	pattern := Pattern{Root: patternTerm}
+
+	// Create a simple replacement
+	replacementTerm := &Term{
+		Foundry: "opennlp",
+		Key:     "COMBINED_DET",
+		Layer:   "p",
+		Match:   MatchEqual,
+	}
+	replacement := Replacement{Root: replacementTerm}
+
+	// Test pattern
+	assert.NotNil(t, pattern.Root)
+	assert.Equal(t, patternTerm, pattern.Root)
+
+	// Test replacement
+	assert.NotNil(t, replacement.Root)
+	assert.Equal(t, replacementTerm, replacement.Root)
+}