Fix layer precedence and document precedences

Change-Id: I7fdc9f9122d7e723f98674a9f9e3010c38118ba9
diff --git a/mapper/response.go b/mapper/response.go
index 6ed425c..e89d12a 100644
--- a/mapper/response.go
+++ b/mapper/response.go
@@ -6,6 +6,7 @@
 
 	"github.com/KorAP/KoralPipe-TermMapper/ast"
 	"github.com/KorAP/KoralPipe-TermMapper/matcher"
+	"github.com/KorAP/KoralPipe-TermMapper/parser"
 	"github.com/rs/zerolog/log"
 )
 
@@ -37,7 +38,7 @@
 
 	// Process the snippet with each rule
 	processedSnippet := snippet
-	for _, rule := range rules {
+	for ruleIndex, rule := range rules {
 		// Create pattern and replacement based on direction
 		var pattern, replacement ast.Node
 		if opts.Direction { // true means AtoB
@@ -56,34 +57,34 @@
 			replacement = token.Wrap
 		}
 
-		// Apply foundry and layer overrides to pattern and replacement
+		// Apply foundry and layer overrides with proper precedence
+		mappingList := m.mappingLists[mappingID]
+
+		// Determine foundry and layer values based on direction
 		var patternFoundry, patternLayer, replacementFoundry, replacementLayer string
-		if opts.Direction { // true means AtoB
+		if opts.Direction { // AtoB
 			patternFoundry, patternLayer = opts.FoundryA, opts.LayerA
 			replacementFoundry, replacementLayer = opts.FoundryB, opts.LayerB
-		} else {
+			// Apply mapping list defaults if not specified
+			if replacementFoundry == "" {
+				replacementFoundry = mappingList.FoundryB
+			}
+			if replacementLayer == "" {
+				replacementLayer = mappingList.LayerB
+			}
+		} else { // BtoA
 			patternFoundry, patternLayer = opts.FoundryB, opts.LayerB
 			replacementFoundry, replacementLayer = opts.FoundryA, opts.LayerA
-		}
-
-		// If foundry/layer are empty in options, get them from the mapping list
-		mappingList := m.mappingLists[mappingID]
-		if replacementFoundry == "" {
-			if opts.Direction { // AtoB
-				replacementFoundry = mappingList.FoundryB
-			} else {
+			// Apply mapping list defaults if not specified
+			if replacementFoundry == "" {
 				replacementFoundry = mappingList.FoundryA
 			}
-		}
-		if replacementLayer == "" {
-			if opts.Direction { // AtoB
-				replacementLayer = mappingList.LayerB
-			} else {
+			if replacementLayer == "" {
 				replacementLayer = mappingList.LayerA
 			}
 		}
 
-		// Clone pattern and apply overrides
+		// Clone pattern and apply foundry and layer overrides
 		processedPattern := pattern.Clone()
 		if patternFoundry != "" || patternLayer != "" {
 			ast.ApplyFoundryAndLayerOverrides(processedPattern, patternFoundry, patternLayer)
@@ -108,9 +109,10 @@
 			continue // No matches, try next rule
 		}
 
-		// Apply RestrictToObligatory to the replacement to get the annotations to add
-		// Note: Only pass foundry override, not layer, since replacement terms have correct layers
-		restrictedReplacement := ast.RestrictToObligatory(replacement, replacementFoundry, "")
+		// Apply RestrictToObligatory with layer precedence logic
+		restrictedReplacement := m.applyReplacementWithLayerPrecedence(
+			replacement, replacementFoundry, replacementLayer,
+			mappingID, ruleIndex, bool(opts.Direction))
 		if restrictedReplacement == nil {
 			continue // Nothing obligatory to add
 		}
@@ -239,3 +241,124 @@
 
 	return result, nil
 }
+
+// applyReplacementWithLayerPrecedence applies RestrictToObligatory with proper layer precedence
+func (m *Mapper) applyReplacementWithLayerPrecedence(
+	replacement ast.Node, foundry, layerOverride string,
+	mappingID string, ruleIndex int, direction bool) ast.Node {
+
+	// First, apply RestrictToObligatory without layer override to preserve explicit layers
+	restricted := ast.RestrictToObligatory(replacement, foundry, "")
+	if restricted == nil {
+		return nil
+	}
+
+	// If no layer override is specified, we're done
+	if layerOverride == "" {
+		return restricted
+	}
+
+	// Apply layer override only to terms that didn't have explicit layers in the original rule
+	mappingList := m.mappingLists[mappingID]
+	if ruleIndex < len(mappingList.Mappings) {
+		originalRule := string(mappingList.Mappings[ruleIndex])
+		m.applySelectiveLayerOverrides(restricted, layerOverride, originalRule, direction)
+	}
+
+	return restricted
+}
+
+// applySelectiveLayerOverrides applies layer overrides only to terms without explicit layers
+func (m *Mapper) applySelectiveLayerOverrides(node ast.Node, layerOverride, originalRule string, direction bool) {
+	if node == nil {
+		return
+	}
+
+	// Parse the original rule without defaults to detect explicit layers
+	explicitTerms := m.getExplicitTerms(originalRule, direction)
+
+	// Apply overrides only to terms that weren't explicit in the original rule
+	termIndex := 0
+	m.applyLayerOverrideToImplicitTerms(node, layerOverride, explicitTerms, &termIndex)
+}
+
+// getExplicitTerms parses the original rule without defaults to identify terms with explicit layers
+func (m *Mapper) getExplicitTerms(originalRule string, direction bool) map[int]bool {
+	explicitTerms := make(map[int]bool)
+
+	// Parse without defaults to see what was explicitly specified
+	parser, err := parser.NewGrammarParser("", "")
+	if err != nil {
+		return explicitTerms
+	}
+
+	result, err := parser.ParseMapping(originalRule)
+	if err != nil {
+		return explicitTerms
+	}
+
+	// Get the replacement side based on direction
+	var replacement ast.Node
+	if direction { // AtoB
+		replacement = result.Lower.Wrap
+	} else { // BtoA
+		replacement = result.Upper.Wrap
+	}
+
+	// Extract terms and check which ones have explicit layers
+	termIndex := 0
+	m.markExplicitTerms(replacement, explicitTerms, &termIndex)
+	return explicitTerms
+}
+
+// markExplicitTerms recursively marks terms that have explicit layers
+func (m *Mapper) markExplicitTerms(node ast.Node, explicitTerms map[int]bool, termIndex *int) {
+	if node == nil {
+		return
+	}
+
+	switch n := node.(type) {
+	case *ast.Term:
+		// A term has an explicit layer if it was specified in the original rule
+		if n.Layer != "" {
+			explicitTerms[*termIndex] = true
+		}
+		*termIndex++
+
+	case *ast.TermGroup:
+		for _, operand := range n.Operands {
+			m.markExplicitTerms(operand, explicitTerms, termIndex)
+		}
+
+	case *ast.Token:
+		if n.Wrap != nil {
+			m.markExplicitTerms(n.Wrap, explicitTerms, termIndex)
+		}
+	}
+}
+
+// applyLayerOverrideToImplicitTerms applies layer override only to terms not marked as explicit
+func (m *Mapper) applyLayerOverrideToImplicitTerms(node ast.Node, layerOverride string, explicitTerms map[int]bool, termIndex *int) {
+	if node == nil {
+		return
+	}
+
+	switch n := node.(type) {
+	case *ast.Term:
+		// Apply override only if this term wasn't explicit in the original rule
+		if !explicitTerms[*termIndex] && n.Layer != "" {
+			n.Layer = layerOverride
+		}
+		*termIndex++
+
+	case *ast.TermGroup:
+		for _, operand := range n.Operands {
+			m.applyLayerOverrideToImplicitTerms(operand, layerOverride, explicitTerms, termIndex)
+		}
+
+	case *ast.Token:
+		if n.Wrap != nil {
+			m.applyLayerOverrideToImplicitTerms(n.Wrap, layerOverride, explicitTerms, termIndex)
+		}
+	}
+}
diff --git a/mapper/response_test.go b/mapper/response_test.go
index 76fd45e..566f235 100644
--- a/mapper/response_test.go
+++ b/mapper/response_test.go
@@ -9,484 +9,6 @@
 	"github.com/stretchr/testify/require"
 )
 
-func XTestResponseMapping(t *testing.T) {
-
-	responseSnippet := `{
-  "@context": "http://korap.ids-mannheim.de/ns/KoralQuery/v0.3/context.jsonld",
-  "ID": null,
-  "author": "Schmelzle, u.a.",
-  "availability": "CC-BY-SA",
-  "context": {
-    "left": [
-      "token",
-      0
-    ],
-    "right": [
-      "token",
-      0
-    ]
-  },
-  "corpusID": null,
-  "corpusSigle": "WPD17",
-  "docID": null,
-  "docSigle": "WPD17/J80",
-  "fields": [
-    {
-      "@type": "koral:field",
-      "key": "ID"
-    },
-    {
-      "@type": "koral:field",
-      "key": "textSigle",
-      "type": "type:string",
-      "value": "WPD17/J80/33968"
-    },
-    {
-      "@type": "koral:field",
-      "key": "corpusID"
-    },
-    {
-      "@type": "koral:field",
-      "key": "author",
-      "type": "type:text",
-      "value": "Schmelzle, u.a."
-    },
-    {
-      "@type": "koral:field",
-      "key": "title",
-      "type": "type:text",
-      "value": "Johanne von Gemmingen"
-    },
-    {
-      "@type": "koral:field",
-      "key": "subTitle"
-    },
-    {
-      "@type": "koral:field",
-      "key": "textClass"
-    },
-    {
-      "@type": "koral:field",
-      "key": "pubPlace",
-      "type": "type:string",
-      "value": "URL:http://de.wikipedia.org"
-    },
-    {
-      "@type": "koral:field",
-      "key": "pubDate",
-      "type": "type:date",
-      "value": "2017-07-01"
-    },
-    {
-      "@type": "koral:field",
-      "key": "availability",
-      "type": "type:string",
-      "value": "CC-BY-SA"
-    },
-    {
-      "@type": "koral:field",
-      "key": "layerInfos",
-      "type": "type:store",
-      "value": "corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens"
-    },
-    {
-      "@type": "koral:field",
-      "key": "docSigle",
-      "type": "type:string",
-      "value": "WPD17/J80"
-    },
-    {
-      "@type": "koral:field",
-      "key": "corpusSigle",
-      "type": "type:string",
-      "value": "WPD17"
-    }
-  ],
-  "hasSnippet": true,
-  "hasTokens": false,
-  "layerInfos": "corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens",
-  "matchID": "p162-165(1)163-163x_yuvMM6VZLzLe_qZ0zb9yguvk37eDi-pSoL1nBdUkhNs",
-  "meta": {
-    "version": "Krill-0.64.1"
-  },
-  "pubDate": "2017-07-01",
-  "pubPlace": "URL:http://de.wikipedia.org",
-  "snippet": "<span class=\"context-left\">` +
-		`</span>` +
-		`<span class=\"match\">` +
-		`<mark>` +
-		`<span title=\"corenlp/p:ART\">` +
-		`<span title=\"marmot/m:case:nom\">` +
-		`<span title=\"marmot/m:gender:masc\">` +
-		`<span title=\"marmot/m:number:sg\">` +
-		`<span title=\"marmot/p:ART\">` +
-		`<span title=\"opennlp/p:ART\">` +
-		`<span title=\"tt/l:die\">` +
-		`<span title=\"tt/p:ART\">Der</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span> ` +
-		`<span title=\"corenlp/p:ADJA\">` +
-		`<span title=\"marmot/m:case:nom\">` +
-		`<span title=\"marmot/m:degree:pos\">` +
-		`<span title=\"marmot/m:gender:masc\">` +
-		`<span title=\"marmot/m:number:sg\">` +
-		`<span title=\"marmot/p:ADJA\">` +
-		`<span title=\"opennlp/p:ADJA\">` +
-		`<span title=\"tt/l:alt\">` +
-		`<span title=\"tt/p:ADJA\">alte</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span> ` +
-		`<span title=\"corenlp/p:NN\">` +
-		`<span title=\"marmot/m:case:nom\">` +
-		`<span title=\"marmot/m:gender:masc\">` +
-		`<span title=\"marmot/m:number:sg\">` +
-		`<span title=\"marmot/p:NN\">` +
-		`<span title=\"opennlp/p:NN\">` +
-		`<span title=\"tt/l:Baum\">` +
-		`<span title=\"tt/p:NN\">Baum</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</mark> ` +
-		`<span title=\"corenlp/p:KON\">` +
-		`<span title=\"marmot/p:KON\">` +
-		`<span title=\"opennlp/p:KON\">` +
-		`<span title=\"tt/l:und\">` +
-		`<span title=\"tt/p:KON\">und</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span> ` +
-		`<span title=\"corenlp/p:ADJA\">` +
-		`<span title=\"marmot/m:case:nom\">` +
-		`<span title=\"marmot/m:degree:pos\">` +
-		`<span title=\"marmot/m:gender:masc\">` +
-		`<span title=\"marmot/m:number:pl\">` +
-		`<span title=\"marmot/p:ADJA\">` +
-		`<span title=\"opennlp/p:ADJA\">` +
-		`<span title=\"tt/l:andere\">` +
-		`<span title=\"tt/p:PIAT\">` +
-		`<span title=\"tt/p:PIS\">andere</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span> ` +
-		`<span title=\"corenlp/p:NN\">` +
-		`<span title=\"marmot/m:case:nom\">` +
-		`<span title=\"marmot/m:gender:masc\">` +
-		`<span title=\"marmot/m:number:pl\">` +
-		`<span title=\"marmot/p:NN\">` +
-		`<span title=\"opennlp/p:NN\">` +
-		`<span title=\"tt/l:Märchen\">` +
-		`<span title=\"tt/p:NN\">Märchen</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>, ` +
-		`<span title=\"corenlp/p:CARD\">` +
-		`<span title=\"marmot/p:CARD\">` +
-		`<span title=\"opennlp/p:CARD\">` +
-		`<span title=\"tt/l:@card@\">` +
-		`<span title=\"tt/p:CARD\">1946</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span> ` +
-		`</span>` +
-		`<span class=\"context-right\"></span>",` +
-		`"subTitle": null,
-  "textClass": null,
-  "textID": null,
-  "textSigle": "WPD17/J80/33968",
-  "title": "Johanne von Gemmingen"
-}`
-
-	expectedOutput := `{
-  "@context": "http://korap.ids-mannheim.de/ns/KoralQuery/v0.3/context.jsonld",
-  "ID": null,
-  "author": "Schmelzle, u.a.",
-  "availability": "CC-BY-SA",
-  "context": {
-    "left": [
-      "token",
-      0
-    ],
-    "right": [
-      "token",
-      0
-    ]
-  },
-  "corpusID": null,
-  "corpusSigle": "WPD17",
-  "docID": null,
-  "docSigle": "WPD17/J80",
-  "fields": [
-    {
-      "@type": "koral:field",
-      "key": "ID"
-    },
-    {
-      "@type": "koral:field",
-      "key": "textSigle",
-      "type": "type:string",
-      "value": "WPD17/J80/33968"
-    },
-    {
-      "@type": "koral:field",
-      "key": "corpusID"
-    },
-    {
-      "@type": "koral:field",
-      "key": "author",
-      "type": "type:text",
-      "value": "Schmelzle, u.a."
-    },
-    {
-      "@type": "koral:field",
-      "key": "title",
-      "type": "type:text",
-      "value": "Johanne von Gemmingen"
-    },
-    {
-      "@type": "koral:field",
-      "key": "subTitle"
-    },
-    {
-      "@type": "koral:field",
-      "key": "textClass"
-    },
-    {
-      "@type": "koral:field",
-      "key": "pubPlace",
-      "type": "type:string",
-      "value": "URL:http://de.wikipedia.org"
-    },
-    {
-      "@type": "koral:field",
-      "key": "pubDate",
-      "type": "type:date",
-      "value": "2017-07-01"
-    },
-    {
-      "@type": "koral:field",
-      "key": "availability",
-      "type": "type:string",
-      "value": "CC-BY-SA"
-    },
-    {
-      "@type": "koral:field",
-      "key": "layerInfos",
-      "type": "type:store",
-      "value": "corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens"
-    },
-    {
-      "@type": "koral:field",
-      "key": "docSigle",
-      "type": "type:string",
-      "value": "WPD17/J80"
-    },
-    {
-      "@type": "koral:field",
-      "key": "corpusSigle",
-      "type": "type:string",
-      "value": "WPD17"
-    }
-  ],
-  "hasSnippet": true,
-  "hasTokens": false,
-  "layerInfos": "corenlp/c=spans corenlp/p=tokens corenlp/s=spans dereko/s=spans malt/d=rels marmot/m=tokens marmot/p=tokens opennlp/p=tokens opennlp/s=spans tt/l=tokens tt/p=tokens",
-  "matchID": "p162-165(1)163-163x_yuvMM6VZLzLe_qZ0zb9yguvk37eDi-pSoL1nBdUkhNs",
-  "meta": {
-    "version": "Krill-0.64.1"
-  },
-  "pubDate": "2017-07-01",
-  "pubPlace": "URL:http://de.wikipedia.org",
-  "snippet": "<span class=\"context-left\">` +
-		`</span>` +
-		`<span class=\"match\">` +
-		`<mark>` +
-		`<span title=\"corenlp/p:ART\">` +
-		`<span title=\"marmot/m:case:nom\">` +
-		`<span title=\"marmot/m:gender:masc\">` +
-		`<span title=\"marmot/m:number:sg\">` +
-		`<span title=\"marmot/p:ART\">` +
-		`<span title=\"opennlp/p:ART\">` +
-		`<span title=\"tt/l:die\">` +
-		`<span title=\"tt/p:ART\">` +
-		`<span title=\"opennlp/p:M\" class=\"notinindex\">` +
-		`<span title=\"opennlp/m:M\" class=\"notinindex\">Der</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span> ` +
-		`<span title=\"corenlp/p:ADJA\">` +
-		`<span title=\"marmot/m:case:nom\">` +
-		`<span title=\"marmot/m:degree:pos\">` +
-		`<span title=\"marmot/m:gender:masc\">` +
-		`<span title=\"marmot/m:number:sg\">` +
-		`<span title=\"marmot/p:ADJA\">` +
-		`<span title=\"opennlp/p:ADJA\">` +
-		`<span title=\"tt/l:alt\">` +
-		`<span title=\"tt/p:ADJA\">` +
-		`<span title=\"opennlp/p:M\" class=\"notinindex\">` +
-		`<span title=\"opennlp/m:M\" class=\"notinindex\">alte</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span> ` +
-		`<span title=\"corenlp/p:NN\">` +
-		`<span title=\"marmot/m:case:nom\">` +
-		`<span title=\"marmot/m:gender:masc\">` +
-		`<span title=\"marmot/m:number:sg\">` +
-		`<span title=\"marmot/p:NN\">` +
-		`<span title=\"opennlp/p:NN\">` +
-		`<span title=\"tt/l:Baum\">` +
-		`<span title=\"tt/p:NN\">` +
-		`<span title=\"opennlp/p:M\" class=\"notinindex\">` +
-		`<span title=\"opennlp/m:M\" class=\"notinindex\">Baum</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</mark> ` +
-		`<span title=\"corenlp/p:KON\">` +
-		`<span title=\"marmot/p:KON\">` +
-		`<span title=\"opennlp/p:KON\">` +
-		`<span title=\"tt/l:und\">` +
-		`<span title=\"tt/p:KON\">und</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span> ` +
-		`<span title=\"corenlp/p:ADJA\">` +
-		`<span title=\"marmot/m:case:nom\">` +
-		`<span title=\"marmot/m:degree:pos\">` +
-		`<span title=\"marmot/m:gender:masc\">` +
-		`<span title=\"marmot/m:number:pl\">` +
-		`<span title=\"marmot/p:ADJA\">` +
-		`<span title=\"opennlp/p:ADJA\">` +
-		`<span title=\"tt/l:andere\">` +
-		`<span title=\"tt/p:PIAT\">` +
-		`<span title=\"tt/p:PIS\">` +
-		`<span title=\"opennlp/p:M\" class=\"notinindex\">` +
-		`<span title=\"opennlp/m:M\" class=\"notinindex\">andere</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span> ` +
-		`<span title=\"corenlp/p:NN\">` +
-		`<span title=\"marmot/m:case:nom\">` +
-		`<span title=\"marmot/m:gender:masc\">` +
-		`<span title=\"marmot/m:number:pl\">` +
-		`<span title=\"marmot/p:NN\">` +
-		`<span title=\"opennlp/p:NN\">` +
-		`<span title=\"tt/l:Märchen\">` +
-		`<span title=\"tt/p:NN\">` +
-		`<span title=\"opennlp/p:M\" class=\"notinindex\">` +
-		`<span title=\"opennlp/p:M\" class=\"notinindex\">Märchen</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>, ` +
-		`<span title=\"corenlp/p:CARD\">` +
-		`<span title=\"marmot/p:CARD\">` +
-		`<span title=\"opennlp/p:CARD\">` +
-		`<span title=\"tt/l:@card@\">` +
-		`<span title=\"tt/p:CARD\">1946</span>` +
-		`</span>` +
-		`</span>` +
-		`</span>` +
-		`</span> ` +
-		`</span>` +
-		`<span class=\"context-right\"></span>",` +
-		`"subTitle": null,
-  "textClass": null,
-  "textID": null,
-  "textSigle": "WPD17/J80/33968",
-  "title": "Johanne von Gemmingen"
-}`
-
-	// Create test mapping list specifically for token to termGroup test
-	mappingList := config.MappingList{
-		ID:       "test-mapper",
-		FoundryA: "marmot",
-		LayerA:   "m",
-		FoundryB: "opennlp", // Keep the same foundry for both sides
-		LayerB:   "p",
-		Mappings: []config.MappingRule{
-			"[gender=masc] <> [opennlp/p=M & opennlp/m=M]",
-		},
-	}
-
-	// Create a new mapper
-	m, err := NewMapper([]config.MappingList{mappingList})
-	require.NoError(t, err)
-
-	var inputData any
-	err = json.Unmarshal([]byte(responseSnippet), &inputData)
-	assert.Nil(t, err)
-
-	result, err := m.ApplyResponseMappings("test-mapper", MappingOptions{Direction: AtoB}, inputData)
-	assert.Nil(t, err)
-
-	var expectedData any
-	err = json.Unmarshal([]byte(expectedOutput), &expectedData)
-
-	assert.Equal(t, expectedData, result)
-	assert.Nil(t, err)
-}
-
 // TestResponseMappingAnnotationCreation tests creating new annotations based on RestrictToObligatory
 func TestResponseMappingAnnotationCreation(t *testing.T) {
 	// Simple snippet with a single annotated token
@@ -970,3 +492,86 @@
 	author := resultMap["author"].(string)
 	assert.Equal(t, "John Doe", author)
 }
+
+// TestResponseMappingWithLayerOverride tests layer precedence rules
+func TestResponseMappingWithLayerOverride(t *testing.T) {
+	// Test 1: Explicit layer in mapping rule should take precedence over MappingOptions
+	t.Run("Explicit layer takes precedence", func(t *testing.T) {
+		responseSnippet := `{
+			"snippet": "<span title=\"marmot/p:DET\">Der</span>"
+		}`
+
+		// Mapping rule with explicit layer [p=DT] - this should NOT be overridden
+		mappingList := config.MappingList{
+			ID:       "test-layer-precedence",
+			FoundryA: "marmot",
+			LayerA:   "p",
+			FoundryB: "opennlp",
+			LayerB:   "p", // default layer
+			Mappings: []config.MappingRule{
+				"[DET] <> [p=DT]", // Explicit layer "p" should not be overridden
+			},
+		}
+
+		m, err := NewMapper([]config.MappingList{mappingList})
+		require.NoError(t, err)
+
+		var inputData any
+		err = json.Unmarshal([]byte(responseSnippet), &inputData)
+		require.NoError(t, err)
+
+		// Apply with layer override - should NOT affect explicit layer in mapping rule
+		result, err := m.ApplyResponseMappings("test-layer-precedence", MappingOptions{
+			Direction: AtoB,
+			LayerB:    "pos", // This should NOT override the explicit "p" layer in [p=DT]
+		}, inputData)
+		require.NoError(t, err)
+
+		resultMap := result.(map[string]any)
+		snippet := resultMap["snippet"].(string)
+
+		// Should use explicit layer "p" from mapping rule, NOT "pos" from override
+		assert.Contains(t, snippet, `title="opennlp/p:DT" class="notinindex"`)
+		assert.NotContains(t, snippet, `title="opennlp/pos:DT" class="notinindex"`)
+	})
+
+	// Test 2: Implicit layer in mapping rule should use MappingOptions layer override
+	t.Run("Implicit layer uses MappingOptions override", func(t *testing.T) {
+		responseSnippet := `{
+			"snippet": "<span title=\"marmot/p:DET\">Der</span>"
+		}`
+
+		// Mapping rule with implicit layer [DT] - this should use layer override
+		mappingList := config.MappingList{
+			ID:       "test-layer-override",
+			FoundryA: "marmot",
+			LayerA:   "p",
+			FoundryB: "opennlp",
+			LayerB:   "p", // default layer
+			Mappings: []config.MappingRule{
+				"[DET] <> [DT]", // No explicit layer - should use override
+			},
+		}
+
+		m, err := NewMapper([]config.MappingList{mappingList})
+		require.NoError(t, err)
+
+		var inputData any
+		err = json.Unmarshal([]byte(responseSnippet), &inputData)
+		require.NoError(t, err)
+
+		// Apply with layer override - should affect implicit layer in mapping rule
+		result, err := m.ApplyResponseMappings("test-layer-override", MappingOptions{
+			Direction: AtoB,
+			LayerB:    "pos", // This should override the default layer for [DT]
+		}, inputData)
+		require.NoError(t, err)
+
+		resultMap := result.(map[string]any)
+		snippet := resultMap["snippet"].(string)
+
+		// Should use layer "pos" from override, NOT default "p" layer
+		assert.Contains(t, snippet, `title="opennlp/pos:DT" class="notinindex"`)
+		assert.NotContains(t, snippet, `title="opennlp/p:DT" class="notinindex"`)
+	})
+}