Add RestrictObligatory filter for response rewriting

Change-Id: I30a386ac48fa8dcbd0635b77fa6449c755f0cd59
diff --git a/ast/ast.go b/ast/ast.go
index e47a78e..2da55b8 100644
--- a/ast/ast.go
+++ b/ast/ast.go
@@ -308,3 +308,92 @@
 		}
 	}
 }
+
+// RestrictToObligatory takes a replacement node from a mapping rule and reduces the boolean structure
+// to only obligatory operations by removing optional OR-relations and keeping required AND-relations.
+// It also applies foundry and layer overrides like ApplyFoundryAndLayerOverrides().
+// Note: This function is designed for mapping rule replacement nodes and does not handle CatchallNodes.
+// For efficiency, restriction is performed first, then foundry/layer overrides are applied to the smaller result.
+//
+// Examples:
+//   - (a & b & c) -> (a & b & c) (kept as is)
+//   - (a & b & (c | d) & e) -> (a & b & e) (OR-relation removed)
+//   - (a | b) -> nil (completely optional)
+func RestrictToObligatory(node Node, foundry, layer string) Node {
+	if node == nil {
+		return nil
+	}
+
+	// First, clone and restrict to obligatory operations
+	cloned := node.Clone()
+	restricted := restrictToObligatoryRecursive(cloned)
+
+	// Then apply foundry and layer overrides to the smaller, restricted tree
+	if restricted != nil {
+		ApplyFoundryAndLayerOverrides(restricted, foundry, layer)
+	}
+
+	return restricted
+}
+
+// restrictToObligatoryRecursive performs the actual restriction logic
+func restrictToObligatoryRecursive(node Node) Node {
+	if node == nil {
+		return nil
+	}
+
+	switch n := node.(type) {
+	case *Term:
+		// Terms are always obligatory
+		return n
+
+	case *Token:
+		// Process the wrapped node
+		if n.Wrap != nil {
+			restricted := restrictToObligatoryRecursive(n.Wrap)
+			if restricted == nil {
+				return nil
+			}
+			return &Token{
+				Wrap:     restricted,
+				Rewrites: n.Rewrites,
+			}
+		}
+		return n
+
+	case *TermGroup:
+		if n.Relation == OrRelation {
+			// OR-relations are optional, so remove them
+			return nil
+		} else if n.Relation == AndRelation {
+			// AND-relations are obligatory, but we need to process operands
+			var obligatoryOperands []Node
+			for _, operand := range n.Operands {
+				restricted := restrictToObligatoryRecursive(operand)
+				if restricted != nil {
+					obligatoryOperands = append(obligatoryOperands, restricted)
+				}
+			}
+
+			// If no operands remain, return nil
+			if len(obligatoryOperands) == 0 {
+				return nil
+			}
+
+			// If only one operand remains, return it directly
+			if len(obligatoryOperands) == 1 {
+				return obligatoryOperands[0]
+			}
+
+			// Return the group with obligatory operands
+			return &TermGroup{
+				Operands: obligatoryOperands,
+				Relation: AndRelation,
+				Rewrites: n.Rewrites,
+			}
+		}
+	}
+
+	// For unknown node types, return as is
+	return node
+}
diff --git a/ast/ast_test.go b/ast/ast_test.go
index 49fdbe3..3965f1d 100644
--- a/ast/ast_test.go
+++ b/ast/ast_test.go
@@ -851,3 +851,324 @@
 	assert.Equal(t, "original_foundry", term.Foundry)
 	assert.Equal(t, "original_layer", term.Layer)
 }
+
+func TestRestrictToObligatory(t *testing.T) {
+	tests := []struct {
+		name     string
+		node     Node
+		foundry  string
+		layer    string
+		expected Node
+	}{
+		{
+			name: "Simple term - kept as is",
+			node: &Term{
+				Foundry: "old_foundry",
+				Key:     "DET",
+				Layer:   "old_layer",
+				Match:   MatchEqual,
+			},
+			foundry: "new_foundry",
+			layer:   "new_layer",
+			expected: &Term{
+				Foundry: "new_foundry",
+				Key:     "DET",
+				Layer:   "new_layer",
+				Match:   MatchEqual,
+			},
+		},
+		{
+			name: "AND group - kept as is",
+			node: &TermGroup{
+				Operands: []Node{
+					&Term{
+						Foundry: "old_foundry",
+						Key:     "A",
+						Layer:   "old_layer",
+						Match:   MatchEqual,
+					},
+					&Term{
+						Foundry: "old_foundry",
+						Key:     "B",
+						Layer:   "old_layer",
+						Match:   MatchEqual,
+					},
+					&Term{
+						Foundry: "old_foundry",
+						Key:     "C",
+						Layer:   "old_layer",
+						Match:   MatchEqual,
+					},
+				},
+				Relation: AndRelation,
+			},
+			foundry: "new_foundry",
+			layer:   "new_layer",
+			expected: &TermGroup{
+				Operands: []Node{
+					&Term{
+						Foundry: "new_foundry",
+						Key:     "A",
+						Layer:   "new_layer",
+						Match:   MatchEqual,
+					},
+					&Term{
+						Foundry: "new_foundry",
+						Key:     "B",
+						Layer:   "new_layer",
+						Match:   MatchEqual,
+					},
+					&Term{
+						Foundry: "new_foundry",
+						Key:     "C",
+						Layer:   "new_layer",
+						Match:   MatchEqual,
+					},
+				},
+				Relation: AndRelation,
+			},
+		},
+		{
+			name: "OR group - becomes nil",
+			node: &TermGroup{
+				Operands: []Node{
+					&Term{
+						Foundry: "old_foundry",
+						Key:     "A",
+						Layer:   "old_layer",
+						Match:   MatchEqual,
+					},
+					&Term{
+						Foundry: "old_foundry",
+						Key:     "B",
+						Layer:   "old_layer",
+						Match:   MatchEqual,
+					},
+				},
+				Relation: OrRelation,
+			},
+			foundry:  "new_foundry",
+			layer:    "new_layer",
+			expected: nil,
+		},
+		{
+			name: "Mixed AND with nested OR - OR removed",
+			node: &TermGroup{
+				Operands: []Node{
+					&Term{
+						Foundry: "old_foundry",
+						Key:     "A",
+						Layer:   "old_layer",
+						Match:   MatchEqual,
+					},
+					&Term{
+						Foundry: "old_foundry",
+						Key:     "B",
+						Layer:   "old_layer",
+						Match:   MatchEqual,
+					},
+					&TermGroup{
+						Operands: []Node{
+							&Term{
+								Foundry: "old_foundry",
+								Key:     "C",
+								Layer:   "old_layer",
+								Match:   MatchEqual,
+							},
+							&Term{
+								Foundry: "old_foundry",
+								Key:     "D",
+								Layer:   "old_layer",
+								Match:   MatchEqual,
+							},
+						},
+						Relation: OrRelation,
+					},
+					&Term{
+						Foundry: "old_foundry",
+						Key:     "E",
+						Layer:   "old_layer",
+						Match:   MatchEqual,
+					},
+				},
+				Relation: AndRelation,
+			},
+			foundry: "new_foundry",
+			layer:   "new_layer",
+			expected: &TermGroup{
+				Operands: []Node{
+					&Term{
+						Foundry: "new_foundry",
+						Key:     "A",
+						Layer:   "new_layer",
+						Match:   MatchEqual,
+					},
+					&Term{
+						Foundry: "new_foundry",
+						Key:     "B",
+						Layer:   "new_layer",
+						Match:   MatchEqual,
+					},
+					&Term{
+						Foundry: "new_foundry",
+						Key:     "E",
+						Layer:   "new_layer",
+						Match:   MatchEqual,
+					},
+				},
+				Relation: AndRelation,
+			},
+		},
+		{
+			name: "AND group with all OR operands - becomes nil",
+			node: &TermGroup{
+				Operands: []Node{
+					&TermGroup{
+						Operands: []Node{
+							&Term{Key: "A", Match: MatchEqual},
+							&Term{Key: "B", Match: MatchEqual},
+						},
+						Relation: OrRelation,
+					},
+					&TermGroup{
+						Operands: []Node{
+							&Term{Key: "C", Match: MatchEqual},
+							&Term{Key: "D", Match: MatchEqual},
+						},
+						Relation: OrRelation,
+					},
+				},
+				Relation: AndRelation,
+			},
+			foundry:  "",
+			layer:    "",
+			expected: nil,
+		},
+		{
+			name: "AND group with one operand after restriction - returns single operand",
+			node: &TermGroup{
+				Operands: []Node{
+					&Term{
+						Foundry: "old_foundry",
+						Key:     "A",
+						Layer:   "old_layer",
+						Match:   MatchEqual,
+					},
+					&TermGroup{
+						Operands: []Node{
+							&Term{Key: "B", Match: MatchEqual},
+							&Term{Key: "C", Match: MatchEqual},
+						},
+						Relation: OrRelation,
+					},
+				},
+				Relation: AndRelation,
+			},
+			foundry: "new_foundry",
+			layer:   "new_layer",
+			expected: &Term{
+				Foundry: "new_foundry",
+				Key:     "A",
+				Layer:   "new_layer",
+				Match:   MatchEqual,
+			},
+		},
+		{
+			name: "Token with wrapped term",
+			node: &Token{
+				Wrap: &Term{
+					Foundry: "old_foundry",
+					Key:     "DET",
+					Layer:   "old_layer",
+					Match:   MatchEqual,
+				},
+				Rewrites: []Rewrite{
+					{Editor: "test"},
+				},
+			},
+			foundry: "new_foundry",
+			layer:   "new_layer",
+			expected: &Token{
+				Wrap: &Term{
+					Foundry: "new_foundry",
+					Key:     "DET",
+					Layer:   "new_layer",
+					Match:   MatchEqual,
+				},
+				Rewrites: []Rewrite{
+					{Editor: "test"},
+				},
+			},
+		},
+		{
+			name: "Token with wrapped OR group - becomes nil",
+			node: &Token{
+				Wrap: &TermGroup{
+					Operands: []Node{
+						&Term{Key: "A", Match: MatchEqual},
+						&Term{Key: "B", Match: MatchEqual},
+					},
+					Relation: OrRelation,
+				},
+			},
+			foundry:  "",
+			layer:    "",
+			expected: nil,
+		},
+		{
+			name:     "Nil node",
+			node:     nil,
+			foundry:  "foundry",
+			layer:    "layer",
+			expected: nil,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := RestrictToObligatory(tt.node, tt.foundry, tt.layer)
+			assert.True(t, NodesEqual(tt.expected, result),
+				"Expected: %+v\nGot: %+v", tt.expected, result)
+		})
+	}
+}
+
+func TestRestrictToObligatoryDoesNotModifyOriginal(t *testing.T) {
+	// Test that the original node is not modified
+	original := &TermGroup{
+		Operands: []Node{
+			&Term{
+				Foundry: "original",
+				Key:     "A",
+				Layer:   "original",
+				Match:   MatchEqual,
+			},
+			&TermGroup{
+				Operands: []Node{
+					&Term{Key: "B", Match: MatchEqual},
+					&Term{Key: "C", Match: MatchEqual},
+				},
+				Relation: OrRelation,
+			},
+		},
+		Relation: AndRelation,
+	}
+
+	// Clone to check that original remains unchanged
+	originalClone := original.Clone()
+
+	// Apply restriction
+	result := RestrictToObligatory(original, "new_foundry", "new_layer")
+
+	// Original should be unchanged
+	assert.True(t, NodesEqual(originalClone, original))
+
+	// Result should be different
+	expected := &Term{
+		Foundry: "new_foundry",
+		Key:     "A",
+		Layer:   "new_layer",
+		Match:   MatchEqual,
+	}
+	assert.True(t, NodesEqual(expected, result))
+}