This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new c3722335c18e CAMEL-26632: camel-jbang - AI tools and messages: 
resource: in a ternary, a header copied to itself, a condition under 
otherwise:, variableReceive under parameters:, a Groovy class without its 
import, and text after a Simple function (#29230)
c3722335c18e is described below

commit c3722335c18eaa25f438265078649c659621cc57
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Nov 5 16:51:43 2026 +0100

    CAMEL-26632: camel-jbang - AI tools and messages: resource: in a ternary, a 
header copied to itself, a condition under otherwise:, variableReceive under 
parameters:, a Groovy class without its import, and text after a Simple 
function (#29230)
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../groovy/DefaultGroovyScriptCompiler.java        |   2 +-
 .../camel/language/groovy/GroovyExpression.java    |  11 +-
 .../camel/language/groovy/GroovyLanguage.java      |  65 +++-
 .../groovy/GroovyCompilePreProcessorTest.java      |  40 +++
 .../groovy/GroovyMissingPropertyHintTest.java      |  14 +
 .../camel/language/simple/BaseSimpleParser.java    |   4 +-
 .../camel/language/simple/SimpleSyntaxHints.java   |  76 ++++
 .../language/simple/SimpleSyntaxHintsTest.java     |  27 ++
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |  73 +++-
 .../dsl/jbang/core/commands/ai/EndpointChecks.java |  51 ++-
 .../core/commands/ai/ExpressionEvaluator.java      |  10 +-
 .../dsl/jbang/core/commands/ai/HeaderChecks.java   |  34 ++
 .../core/commands/ai/HeaderPropertyMixups.java     | 393 +++++++++++++++++++++
 .../dsl/jbang/core/commands/ai/LenientRepairs.java |   5 +
 .../dsl/jbang/core/commands/ai/SimpleChecks.java   | 224 +++++++++++-
 .../jbang/core/commands/ai/SourceValidator.java    |   9 +-
 .../commands/ai/AuthoringToolsLenientTest.java     |  67 ++++
 .../ai/ExpressionEvaluatorLanguageTest.java        |   4 +
 .../core/commands/ai/HeaderPropertyMixupsTest.java | 152 +++++++-
 .../commands/ai/SourceValidatorEndpointTest.java   | 101 ++++++
 .../commands/ai/SourceValidatorSimpleTest.java     | 217 +++++++++++-
 .../camel/dsl/yaml/validator/SchemaHints.java      |  49 +++
 .../validator/YamlValidatorPropertyHintTest.java   |  72 ++++
 23 files changed, 1669 insertions(+), 31 deletions(-)

diff --git 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/DefaultGroovyScriptCompiler.java
 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/DefaultGroovyScriptCompiler.java
index de92be2c30f0..e1f354b2c455 100644
--- 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/DefaultGroovyScriptCompiler.java
+++ 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/DefaultGroovyScriptCompiler.java
@@ -365,7 +365,7 @@ public class DefaultGroovyScriptCompiler extends 
ServiceSupport
             try {
                 clazz = shell.getClassLoader().parseClass(code);
             } catch (CompilationFailedException e) {
-                throw GroovyLanguage.compileFailure(e);
+                throw GroovyLanguage.compileFailure(e, shell.getClassLoader());
             }
             if (clazz != null) {
                 String name = clazz.getName();
diff --git 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
index d50b70a400c9..5b6a43a70a86 100644
--- 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
+++ 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyExpression.java
@@ -21,6 +21,7 @@ import java.lang.invoke.MethodHandles;
 import java.lang.invoke.MethodType;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
@@ -142,7 +143,13 @@ public class GroovyExpression extends ExpressionSupport {
                        + "'), or call it from the route with"
                        + " - bean: {ref: " + name + "}";
             } else {
-                hint = "the script variables are " + SCRIPT_VARIABLES_HINT;
+                // JsonOutput.toJson(body) with no import: a class Groovy 
reads as a variable (r27 of the local-model
+                // benchmark wrote such classes without their import)
+                List<String> classes = GroovyLanguage.notImported(name, 
script.getClass().getClassLoader());
+                hint = !classes.isEmpty()
+                        ? name + " is the class " + classes.get(0) + ", which 
the script does not import: add import "
+                          + classes.get(0) + " at the top of the script, or 
write " + classes.get(0)
+                        : "the script variables are " + SCRIPT_VARIABLES_HINT;
             }
             throw new groovy.lang.MissingPropertyException(
                     e.getMessageWithoutLocationText() + " (" + hint + ")", 
name, e.getType());
@@ -187,7 +194,7 @@ public class GroovyExpression extends ExpressionSupport {
                     return name != null
                             ? shell.getClassLoader().parseClass(text, name) : 
shell.getClassLoader().parseClass(text);
                 } catch (CompilationFailedException e) {
-                    throw GroovyLanguage.compileFailure(e);
+                    throw GroovyLanguage.compileFailure(e, 
shell.getClassLoader());
                 }
             });
             c = new CompiledScript(r.context, r.language, generation, 
fileName, scriptClass, constructor(scriptClass));
diff --git 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyLanguage.java
 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyLanguage.java
index c751a6d6e564..4ce69cb88c73 100644
--- 
a/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyLanguage.java
+++ 
b/components/camel-groovy/src/main/java/org/apache/camel/language/groovy/GroovyLanguage.java
@@ -17,7 +17,9 @@
 package org.apache.camel.language.groovy;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ConcurrentMap;
@@ -193,7 +195,7 @@ public class GroovyLanguage extends TypedLanguageSupport 
implements ScriptingLan
             try {
                 return shell.getClassLoader().parseClass(text);
             } catch (CompilationFailedException e) {
-                throw compileFailure(e);
+                throw compileFailure(e, shell.getClassLoader());
             }
         });
         Script gs = ObjectHelper.newInstance(clazz, Script.class);
@@ -302,16 +304,77 @@ public class GroovyLanguage extends TypedLanguageSupport 
implements ScriptingLan
         }
     }
 
+    /**
+     * The packages of the classes a script reaches for that can need an 
import: JsonSlurper is a class of groovy.json.
+     * A class Groovy finds by itself (java.lang, java.util, java.io, 
java.net, groovy.lang, groovy.util) compiles, so a
+     * package here that a Groovy version imports is never asked about.
+     */
+    private static final List<String> NOT_IMPORTED = List.of("groovy.json", 
"groovy.xml", "groovy.time",
+            "groovy.transform", "java.time", "java.time.format", 
"java.time.temporal", "java.util.concurrent",
+            "java.util.concurrent.atomic", "java.util.function", 
"java.util.stream", "java.util.regex", "java.nio.file",
+            "java.nio.charset", "java.text", "java.math", "java.security", 
"java.sql", "org.apache.camel");
+
+    /**
+     * The classes of the simple name in the packages a script does not import 
by default, that the class loader has:
+     * groovy.json.JsonSlurper for JsonSlurper. Empty when the name is not a 
class there.
+     */
+    static List<String> notImported(String simpleName, ClassLoader 
classLoader) {
+        List<String> answer = new ArrayList<>();
+        if (simpleName == null || simpleName.isEmpty() || 
!Character.isUpperCase(simpleName.charAt(0))) {
+            return answer;
+        }
+        for (String pkg : NOT_IMPORTED) {
+            try {
+                Class.forName(pkg + "." + simpleName, false, classLoader);
+                answer.add(pkg + "." + simpleName);
+            } catch (ClassNotFoundException | LinkageError e) {
+                // not a class of this package
+            }
+        }
+        return answer;
+    }
+
     /**
      * A compilation failure whose cause is a class the script imports and the 
classpath does not have: the compiler
      * names the class and nothing else; the message adds what to do, without 
knowing the runtime (CAMEL-24843).
      */
     static RuntimeException compileFailure(CompilationFailedException e) {
+        return compileFailure(e, GroovyLanguage.class.getClassLoader());
+    }
+
+    /**
+     * As {@link #compileFailure(CompilationFailedException)}, with the class 
loader the script was compiled with: a
+     * class it has is not missing, it needs an import. r27 of the local-model 
benchmark wrote new JsonSlurper() with no
+     * import, was told the class is not on the classpath, added a dependency 
for it, and read the log 17 times.
+     */
+    static RuntimeException compileFailure(CompilationFailedException e, 
ClassLoader classLoader) {
         String msg = e.getMessage();
         if (msg != null && msg.contains("unable to resolve class")) {
             String cls = msg.substring(msg.indexOf("unable to resolve class") 
+ "unable to resolve class".length()).trim();
             int end = cls.indexOf('\n');
             cls = (end > 0 ? cls.substring(0, end) : cls).trim();
+            String simple = cls.substring(cls.lastIndexOf('.') + 1);
+            List<String> classes = notImported(simple, classLoader);
+            classes.remove(cls);
+            if (!classes.isEmpty()) {
+                String fqn = classes.get(0);
+                String others = classes.size() > 1 ? " (or " + String.join(", 
", classes.subList(1, classes.size())) + ")" : "";
+                if (cls.contains(".")) {
+                    // import groovy.util.JsonSlurper: the class of that name 
is in another package
+                    return new RuntimeCamelException(
+                            "Groovy cannot resolve the class " + cls + ": the 
class " + simple + " is " + fqn + others
+                                                     + ". Import it as import 
" + fqn + " at the top of the script.",
+                            e);
+                }
+                // the source line of the error shows how the class is used
+                String created = msg.contains("new " + simple + "()") ? "new " 
+ fqn + "()"
+                        : msg.contains("new " + simple + "(") ? "new " + fqn + 
"(...)" : fqn;
+                return new RuntimeCamelException(
+                        "Groovy cannot resolve the class " + simple + ": it is 
" + fqn + others + ", which the script"
+                                                 + " does not import. Add 
import " + fqn + " at the top of the script,"
+                                                 + " or write " + created + 
".",
+                        e);
+            }
             return new RuntimeCamelException(
                     "Groovy cannot resolve the class " + cls + ": it is not on 
the classpath. Add the library that"
                                              + " provides it as a dependency 
of the application: a Maven dependency,"
diff --git 
a/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyCompilePreProcessorTest.java
 
b/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyCompilePreProcessorTest.java
index bac1a4febc84..e222347fa0ed 100644
--- 
a/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyCompilePreProcessorTest.java
+++ 
b/components/camel-groovy/src/test/java/org/apache/camel/language/groovy/GroovyCompilePreProcessorTest.java
@@ -27,6 +27,7 @@ import org.apache.camel.test.junit6.CamelTestSupport;
 import org.junit.jupiter.api.Test;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -66,6 +67,45 @@ public class GroovyCompilePreProcessorTest extends 
CamelTestSupport {
         assertTrue(seen.get(0).contains("import java.util.Locale"), 
seen.get(0));
     }
 
+    /**
+     * r27 connect-service-jms-request-reply step 3: new JsonSlurper() with no 
import was told that the class is not on
+     * the classpath; the model added a dependency for it and read the log 17 
times. The class is there and needs an
+     * import.
+     */
+    @Test
+    public void testAClassThatNeedsAnImportSaysToImportIt() {
+        GroovyLanguage groovy = (GroovyLanguage) 
context.resolveLanguage("groovy");
+        String msg = assertThrows(Exception.class,
+                () -> groovy.evaluate("def stock = new 
JsonSlurper().parseText('[1, 2]')\nstock.size()", null,
+                        Object.class))
+                .getMessage();
+        assertTrue(msg.startsWith("Groovy cannot resolve the class 
JsonSlurper: it is groovy.json.JsonSlurper, which the"
+                                  + " script does not import. Add import 
groovy.json.JsonSlurper at the top of the script,"
+                                  + " or write new 
groovy.json.JsonSlurper()."),
+                msg);
+        assertFalse(msg.contains("not on the classpath"), msg);
+        // a class created with arguments, and a class of the JDK
+        msg = assertThrows(Exception.class,
+                () -> groovy.evaluate("new XmlSlurper(false, false)", null, 
Object.class)).getMessage();
+        assertTrue(msg.contains("it is groovy.xml.XmlSlurper, which the script 
does not import. Add import"
+                                + " groovy.xml.XmlSlurper at the top of the 
script, or write new groovy.xml.XmlSlurper(...)."),
+                msg);
+        msg = assertThrows(Exception.class,
+                () -> groovy.evaluate("new SimpleDateFormat('yyyy').format(new 
Date())", null, Object.class)).getMessage();
+        assertTrue(msg.contains("it is java.text.SimpleDateFormat, which the 
script does not import"), msg);
+        // an import of the class from another package
+        msg = assertThrows(Exception.class,
+                () -> groovy.evaluate("import groovy.util.JsonSlurper\nnew 
JsonSlurper()", null, Object.class))
+                .getMessage();
+        assertTrue(msg.startsWith("Groovy cannot resolve the class 
groovy.util.JsonSlurper: the class JsonSlurper is"
+                                  + " groovy.json.JsonSlurper. Import it as 
import groovy.json.JsonSlurper at the top of"
+                                  + " the script."),
+                msg);
+        // with the import it works
+        assertEquals(2, groovy.evaluate("import groovy.json.JsonSlurper\nnew 
JsonSlurper().parseText('[1, 2]').size()",
+                null, Integer.class));
+    }
+
     @Test
     public void testUnresolvedClassSaysWhatToDo() {
         GroovyLanguage groovy = (GroovyLanguage) 
context.resolveLanguage("groovy");
diff --git 
a/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
 
b/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
index a259aab2d58b..77d5d2189ea3 100644
--- 
a/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
+++ 
b/components/camel-groovy/src/test/java/org/apache/camel/processor/groovy/GroovyMissingPropertyHintTest.java
@@ -179,6 +179,19 @@ public class GroovyMissingPropertyHintTest extends 
CamelTestSupport {
         assertEquals(java.util.Map.of("sku", "A1"), out);
     }
 
+    @Test
+    public void aClassWithNoImportSaysToImportIt() {
+        // JsonOutput.toJson(body) with no import of groovy.json: Groovy reads 
the class as a variable
+        Exception e = assertThrows(Exception.class, () -> 
template.sendBody("direct:class", Map.of("sku", "A1")));
+        MissingPropertyException cause = 
assertInstanceOf(MissingPropertyException.class, e.getCause());
+        assertTrue(cause.getMessage().contains("No such property: 
JsonOutput"), cause.getMessage());
+        assertTrue(cause.getMessage().contains("(JsonOutput is the class 
groovy.json.JsonOutput, which the script does not"
+                                               + " import: add import 
groovy.json.JsonOutput at the top of the script, or"
+                                               + " write 
groovy.json.JsonOutput)"),
+                cause.getMessage());
+        assertFalse(cause.getMessage().contains("the script variables are"), 
cause.getMessage());
+    }
+
     @Test
     public void messageIsAScriptVariableAsTheHintSays() {
         String out = template.requestBodyAndHeader("direct:message", "World", 
"name", "Hello", String.class);
@@ -197,6 +210,7 @@ public class GroovyMissingPropertyHintTest extends 
CamelTestSupport {
                 
from("direct:customer").transform().groovy("body.customer.customerNo");
                 from("direct:header").transform().groovy("headers.skuList.find 
{ it.sku == headers.sku }");
                 
from("direct:message").transform().groovy("message.getHeader('name') + ' ' + 
message.body");
+                
from("direct:class").transform().groovy("JsonOutput.toJson(body)");
             }
         };
     }
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
index 5a7493afc6c9..4450f894acc6 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/BaseSimpleParser.java
@@ -500,7 +500,9 @@ public abstract class BaseSimpleParser {
             } else if (expect == TokenType.whiteSpace && 
token.getType().getType() == TokenType.singleQuote) {
                 hint = ": a literal that contains a single quote must be 
written with double quotes, e.g. \"it's\"";
             } else if (expect == TokenType.whiteSpace) {
-                hint = ": operators and values must be separated by a space";
+                // ${body.lines}.size(): a method after the closing brace of a 
function, rather than a missing space
+                String after = SimpleSyntaxHints.afterFunction(expression, 
previousIndex);
+                hint = after != null ? ": " + after : ": operators and values 
must be separated by a space";
             }
             throw new SimpleParserException(
                     "expected symbol " + expect + " but was " + 
token.getType().getType() + hint, previousIndex);
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
index 08e5db23e289..a059985b0ce3 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleSyntaxHints.java
@@ -20,6 +20,8 @@ import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 /**
  * Hints that turn a Simple parser error into a message that says what to 
write (CAMEL-24703). Every consumer of the
@@ -172,10 +174,84 @@ public final class SimpleSyntaxHints {
             return "Unexpected token " + word + ": text outside ${...} is a 
literal, functions are written as ${body}, "
                    + "${header.name}; did you mean " + rewrite + "?";
         }
+        // ${body.lines}.size() > 0: the method after the closing brace of the 
function
+        int start = Math.min(Math.max(index, 0), expression.length());
+        while (start > 0 && !Character.isWhitespace(expression.charAt(start - 
1))) {
+            start--;
+        }
+        int close = word.startsWith("${") ? closingBrace(word) : -1;
+        String after = close > 0 ? afterFunction(expression, start + close + 
1) : null;
+        if (after != null) {
+            return "Unexpected token " + word + ": " + after;
+        }
         return "Unexpected token " + word + ": text outside ${...} must be " + 
VALUE_FORMS + ", or an operator ("
                + OPERATORS + ")";
     }
 
+    /** The functions whose value can be read further with a field, a method 
or an index, as ${body.lines.size()}. */
+    private static final Pattern READ_FURTHER = Pattern.compile(
+            
"(?:in\\.)?(?:body|headers?|exchangeProperty|variables?|exception)(?:[.\\[?].*)?"
+                                                                + 
"|(?:mandatoryB|b)odyAs\\(.*|messageAs\\(.*");
+
+    /** A path that reads further: .lines, ?.lines, .size(), [0] or ['sku'], 
one after the other. */
+    private static final Pattern PATH = Pattern.compile(
+            "(?:\\??\\.[A-Za-z_]\\w*(?:\\([^()]*\\))?|\\[[^\\]]*])+");
+
+    /**
+     * A method, a field or an index written right after the closing brace of 
a function, as in ${body.lines}.size():
+     * the parser reads it as text, not as part of the function (r27 of the 
local-model benchmark, 18 calls of the
+     * expression tool with it in one step). The hint writes it inside the 
braces: ${body.lines.size()}. Null when the
+     * text at the index does not follow a function whose value can be read 
further, or is a filter, which Simple does
+     * not have.
+     *
+     * @param  expression the expression
+     * @param  index      the index of the first character after the closing 
brace of the function
+     * @return            the hint, or null
+     */
+    public static String afterFunction(String expression, int index) {
+        if (expression == null || index < 3 || index >= expression.length() || 
expression.charAt(index - 1) != '}') {
+            return null;
+        }
+        // the ${ that the brace closes
+        int depth = 0;
+        int open = -1;
+        for (int i = index - 1; i > 0 && open < 0; i--) {
+            char c = expression.charAt(i);
+            if (c == '}') {
+                depth++;
+            } else if (c == '{' && expression.charAt(i - 1) == '$' && --depth 
== 0) {
+                open = i - 1;
+            }
+        }
+        String rest = expression.substring(index);
+        Matcher path = PATH.matcher(rest);
+        if (open < 0 || !path.lookingAt() || 
rest.matches("(?s)\\??\\.[?^$!]\\[.*|\\[\\?\\(.*")) {
+            return null;
+        }
+        String inner = expression.substring(open + 2, index - 1);
+        if (!READ_FURTHER.matcher(inner).matches()) {
+            return null;
+        }
+        String written = path.group();
+        String what = written.endsWith(")") ? "method" : written.endsWith("]") 
? "index" : "field";
+        return "the " + what + " " + written + " is outside the function, as 
text after its closing brace: write it inside"
+               + " the braces, ${" + inner + written + "}";
+    }
+
+    /** The index of the brace that closes the ${ the text starts with, or -1. 
*/
+    private static int closingBrace(String text) {
+        int depth = 0;
+        for (int i = 0; i < text.length(); i++) {
+            if (text.startsWith("${", i)) {
+                depth++;
+                i++;
+            } else if (text.charAt(i) == '}' && --depth == 0) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
     /** The message when an operator has no usable value next to it. */
     public static String unsupportedOperand(String kind, Object operator, 
String expression, int index) {
         String word = wordAt(expression, index);
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
index 359e10a9ffb1..3fc2e73bf94f 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
@@ -170,6 +170,33 @@ public class SimpleSyntaxHintsTest extends 
ExchangeTestSupport {
         
assertThat(SimpleSyntaxHints.unknownFunction("jsonpath:$.status")).doesNotContain("${jsonpath:$.status}");
     }
 
+    /**
+     * r27 of the local-model benchmark, contracts-rest-code-first steps 1 and 
2: ${body.lines}.size() > 0 was told that
+     * operators and values must be separated by a space, and a model sent it 
to the expression tool 18 times. The
+     * method is text after the function; it goes inside the braces.
+     */
+    @Test
+    public void testAMethodAfterTheFunctionGoesInsideIt() {
+        assertThat(predicateError("${body.lines} != null && 
${body.lines}.size() > 0"))
+                .contains("expected symbol whiteSpace but was character: the 
method .size() is outside the function, as"
+                          + " text after its closing brace: write it inside 
the braces, ${body.lines.size()}")
+                .doesNotContain("separated by a space");
+        assertThat(predicateError("${header.foundOrder}.size() > 0"))
+                .contains("Unexpected token ${header.foundOrder}.size(): the 
method .size() is outside the function, as"
+                          + " text after its closing brace: write it inside 
the braces, ${header.foundOrder.size()}");
+        assertThat(predicateError("${body}[0] == 'a'"))
+                .contains("the index [0] is outside the function, as text 
after its closing brace: write it inside the"
+                          + " braces, ${body[0]}");
+        assertThat(predicateError("${variable.order}.lines.size() > 0"))
+                .contains("the method .lines.size() is outside the 
function").contains("${variable.order.lines.size()}");
+        // a filter is not Simple at all, and a function that cannot be read 
further keeps the message it had
+        assertThat(predicateError("${body}?.?[orderId == 'A'] != 
null")).doesNotContain("outside the function");
+        assertThat(predicateError("${date:now:yyyy}.x > 
0")).doesNotContain("outside the function");
+        // the rewrite is what works
+        exchange.getIn().setBody(Map.of("lines", List.of("a", "b")));
+        assertEquals(true, 
context.resolveLanguage("simple").createPredicate("${body.lines.size()} > 
0").matches(exchange));
+    }
+
     @Test
     public void testOperatorAfterOgnlMethod() {
         // CAMEL-24921: an OGNL call on the left of the operator is wrapped as 
the function it is
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index 440aeb9e49b3..0546cb42e9d0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -1742,7 +1742,7 @@ public final class AuthoringTools {
                 } else if (hidden != null) {
                     // the write repairs the shape itself: the problems to fix 
are the ones the shape hid
                     errors = hidden;
-                    repairable = r.applied();
+                    repairable = markShapeLeft(r);
                 }
             }
             if (!errors.isEmpty()) {
@@ -2025,8 +2025,77 @@ public final class AuthoringTools {
         return r.left().stream().anyMatch(e -> 
written.contains(withoutLine(e))) ? null : r.left();
     }
 
-    /** What the write repairs by itself, said with the problems it still 
refuses. */
+    /**
+     * A schema problem of the shape of the YAML: a key where none goes, two 
EIPs in one step, a node of another type.
+     */
+    private static final Pattern SHAPE_PROBLEM = Pattern.compile(
+            "is not defined in the schema|must have at most \\d+ 
propert|found, \\w+ expected|starts in column"
+                                                                 + "|required 
property");
+
+    /** The line a repair or a problem names: "line 101: otherwise: its steps 
put under steps:", "Line 102: ...". */
+    private static final Pattern NAMED_LINE = Pattern.compile("^[Ll]ine 
(\\d+)\\b");
+
+    /** What a repair says when a problem of the shape is left in the lines it 
changed. */
+    static final String SHAPE_LEFT = " (these lines still have errors)";
+
+    /**
+     * The repairs of a refused write, each marked with {@link #SHAPE_LEFT} 
when a problem of the shape is left in the
+     * lines it changed: then the YAML shape does need a change there. r27l-4 
connect-http-client step 3: the items of
+     * an otherwise: list were conditions, which the repair put under steps:, 
where they were still not steps; told that
+     * the shape needed no change from it, the model sent the same lines 16 
times.
+     */
+    private static List<String> markShapeLeft(Repaired r) {
+        String[] lines = r.content().split("\n", -1);
+        List<Integer> shapeLines = new ArrayList<>();
+        for (String problem : r.left()) {
+            Matcher m = NAMED_LINE.matcher(problem);
+            if (m.find() && SHAPE_PROBLEM.matcher(problem).find()) {
+                shapeLines.add(Integer.parseInt(m.group(1)));
+            }
+        }
+        List<String> answer = new ArrayList<>();
+        for (String applied : r.applied()) {
+            Matcher m = NAMED_LINE.matcher(applied);
+            boolean left = false;
+            if (m.find()) {
+                int start = Integer.parseInt(m.group(1));
+                int end = blockEnd(lines, start);
+                left = shapeLines.stream().anyMatch(l -> l >= start && l <= 
end);
+            }
+            answer.add(left ? applied + SHAPE_LEFT : applied);
+        }
+        return answer;
+    }
+
+    /** The last line (1-based) of the block of a line: the lines after it 
that are indented more, or blank. */
+    private static int blockEnd(String[] lines, int line) {
+        if (line < 1 || line > lines.length) {
+            return line;
+        }
+        int indent = YamlLines.countLeadingSpaces(lines[line - 1]);
+        int end = line;
+        for (int i = line; i < lines.length; i++) {
+            if (lines[i].isBlank()) {
+                continue;
+            }
+            if (YamlLines.countLeadingSpaces(lines[i]) <= indent) {
+                break;
+            }
+            end = i + 1;
+        }
+        return end;
+    }
+
+    /**
+     * What the write repairs by itself, said with the problems it still 
refuses: not that the shape needs no change
+     * when the lines of a repair still have a problem of their shape.
+     */
     static String repairableNote(List<String> repairable) {
+        if (repairable.stream().anyMatch(r -> r.endsWith(SHAPE_LEFT))) {
+            return "The write repairs part of the YAML shape by itself (" + 
String.join("; ", repairable) + "), and the"
+                   + " lines marked still have errors of their shape: change 
them as the errors say. The errors are what"
+                   + " the content has with that repaired.";
+        }
         return "The YAML shape needs no change from you: the write repairs it 
by itself (" + String.join("; ", repairable)
                + "). The errors are what the content has with that repaired.";
     }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
index 4fb7b8f7769c..f065e545f227 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
@@ -34,6 +34,7 @@ import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.catalog.EndpointValidationResult;
 import org.apache.camel.catalog.LanguageValidationResult;
 import org.apache.camel.catalog.RuntimeProvider;
+import org.apache.camel.tooling.model.EipModel;
 import org.apache.camel.util.StringHelper;
 import org.apache.camel.util.URISupport;
 
@@ -396,7 +397,7 @@ final class EndpointChecks {
                 }
             }
             if (!result.isSuccess()) {
-                collectEndpointErrors(errors, result, scheme, catalog, 
lineIdx, optionLineMap);
+                collectEndpointErrors(errors, result, scheme, catalog, 
lineIdx, optionLineMap, eipName, fullUri, uriForm);
             }
             checkRegexOptions(errors, fullUri, lineIdx, optionLineMap);
             checkFileLanguageOptions(errors, catalog, fullUri, rawLine, 
lineIdx, optionLineMap);
@@ -964,7 +965,7 @@ final class EndpointChecks {
 
     static void collectEndpointErrors(
             List<String> errors, EndpointValidationResult result, String 
scheme, CamelCatalog catalog,
-            int uriLineIdx, Map<String, Integer> optionLineMap) {
+            int uriLineIdx, Map<String, Integer> optionLineMap, String 
eipName, String fullUri, boolean uriForm) {
         if (result.getUnknown() != null && !"kamelet".equals(scheme)) {
             // the options of a kamelet: endpoint are the parameters of its 
template, which the catalog does not know
             for (String name : result.getUnknown()) {
@@ -977,8 +978,15 @@ final class EndpointChecks {
                 // (cronService) is a fix that makes it worse; say what the 
endpoint takes instead (an entry of
                 // INVENTED_OPTIONS for it says more, and is the one hint)
                 String named = known == null && name.equals(scheme) ? 
namedAfterComponent(catalog, scheme) : null;
+                // variableReceive under parameters: of a to: is a key of the 
to:, which the closest options of the
+                // endpoint do not say
+                String ofTheEip = known == null && named == null
+                        ? optionOfTheEip(catalog, eipName, name, scheme, 
fullUri, uriForm, optionLineMap.containsKey(name))
+                        : null;
                 if (named != null) {
                     sb.append(" (").append(named).append(")");
+                } else if (ofTheEip != null) {
+                    sb.append(" (").append(ofTheEip).append(")");
                 } else if (result.getUnknownSuggestions() != null) {
                     String[] suggestions = 
result.getUnknownSuggestions().get(name);
                     if (suggestions != null && suggestions.length > 0) {
@@ -1035,6 +1043,45 @@ final class EndpointChecks {
         }
     }
 
+    /**
+     * The words for an option of the EIP of the endpoint written as an option 
of the endpoint: variableReceive under
+     * parameters: of a to: is a key of the to:, next to uri:. r27 
connect-service-jms-request-reply step 2: told to
+     * keep the message with variableReceive, a model put it (and pattern 
before it) under parameters:, and was told
+     * only "jms: Unknown option 'variableReceive'". The options of the EIP 
are the ones the catalog has for it, uri
+     * aside. Null when the EIP has no option of that name.
+     */
+    static String optionOfTheEip(
+            CamelCatalog catalog, String eipName, String name, String scheme, 
String fullUri, boolean uriForm,
+            boolean underParameters) {
+        if (eipName == null || "uri".equals(name)) {
+            return null;
+        }
+        String eip = StringHelper.dashToCamelCase(eipName);
+        EipModel model;
+        try {
+            model = catalog.eipModel(eip);
+        } catch (Exception e) {
+            return null;
+        }
+        if (model == null || model.getOptions().stream()
+                .noneMatch(o -> name.equals(o.getName()) && 
"attribute".equals(o.getKind()))) {
+            return null;
+        }
+        if (uriForm) {
+            return name + " is an option of the " + eip + ", not of the " + 
scheme + " endpoint: set it on the " + eip
+                   + ", not in the uri";
+        }
+        String uri = fullUri.contains("?") ? fullUri.substring(0, 
fullUri.indexOf('?')) : fullUri;
+        if (uri.endsWith(":")) {
+            uri = uri.substring(0, uri.length() - 1);
+        }
+        Matcher value = Pattern.compile("[?&]" + Pattern.quote(name) + 
"=([^&]*)").matcher(fullUri);
+        String written = value.find() && !value.group(1).isEmpty() ? 
value.group(1) : "...";
+        return name + " is a key of the " + eip + ":, next to uri:, not an 
option of the " + scheme + " endpoint: "
+               + eip + ": {uri: " + (underParameters ? uri + ", parameters: 
{...}" : "\"" + uri + "\"") + ", " + name
+               + ": " + written + "}";
+    }
+
     static void addInvalid(
             List<String> errors, Map<String, String> invalid, String type, 
String scheme, int uriLineIdx,
             Map<String, Integer> optionLineMap) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
index e3c82db128bc..6df292ee6053 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
@@ -289,6 +289,11 @@ public final class ExpressionEvaluator {
      * the path ends with the filter, and the type of the value gives the one 
value of a path that goes on after it.
      */
     static String filterNote(String lang, String expression, String status) {
+        if ("simple".equals(lang)) {
+            // a JsonPath or SpEL filter in or after a function, which Simple 
does not have: the jsonpath step that does
+            // it, also when the parser only says that a space is missing (r27 
rest-code-first step 1)
+            return SimpleChecks.filterInFunction(expression);
+        }
         if (!"jsonpath".equals(lang) || expression == null || 
!expression.contains("[?(") || !"ok".equals(status)) {
             return null;
         }
@@ -340,12 +345,13 @@ public final class ExpressionEvaluator {
             // ${...} that an integration before Camel 4.24 refuses, go with 
its message
             String hint = xpathHint(r.language(), r.expression(), 
e.getMessage());
             String elvis = elvisNote(ctx, r.language(), r.expression());
-            if (hint == null && ignored == null && elvis == null) {
+            String filter = filterNote(r.language(), r.expression(), "error");
+            if (hint == null && ignored == null && elvis == null && filter == 
null) {
                 throw e;
             }
             throw new ToolExecutionException(
                     e.getMessage() + (hint != null ? "; " + hint : "") + 
(ignored != null ? "; " + ignored : "")
-                                             + (elvis != null ? "; " + elvis : 
""));
+                                             + (elvis != null ? "; " + elvis : 
"") + (filter != null ? "; " + filter : ""));
         }
         JsonObject out = null;
         try {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java
index 477e61ddba99..5a4236b37a1b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java
@@ -225,6 +225,39 @@ final class HeaderChecks {
         return msgs;
     }
 
+    /** ${body[CamelTimerCounter]} or ${body.CamelTimerCounter}: a field of 
the body named as Camel names its own. */
+    private static final Pattern CAMEL_NAME_ON_BODY = Pattern.compile(
+            
"\\$\\{body(?:\\??\\.|\\[\\s*['\"]?)(Camel[A-Z][A-Za-z0-9]*)['\"]?\\s*]?");
+
+    /**
+     * An exchange property of a component of the file read as a field of the 
body: r27 transform-groovy step 3 wrote
+     * resource:file:order-${body[CamelTimerCounter]}.json, and the body has 
no such field, so the route loaded
+     * order-.json. Once per name.
+     */
+    static List<String> propertiesReadOnTheBody(String content, Set<String> 
schemes) {
+        List<String> msgs = new ArrayList<>();
+        String[] lines = content.split("\n", -1);
+        Set<String> reported = new HashSet<>();
+        for (int i = 0; i < lines.length; i++) {
+            Matcher m = CAMEL_NAME_ON_BODY.matcher(lines[i]);
+            while (m.find()) {
+                String name = m.group(1);
+                for (String scheme : schemes) {
+                    if (EXCHANGE_PROPERTIES.getOrDefault(scheme, 
List.of()).contains(name) && reported.add(name)) {
+                        msgs.add("Line " + (i + 1) + ": " + m.group() + "} 
reads a field of the body, and the body has"
+                                 + " no field " + name + ": " + name
+                                 + " is an exchange property of the " + scheme 
+ ", write ${exchangeProperty." + name
+                                 + "}" + (setsExchangeProperties(scheme, 
content)
+                                         ? ""
+                                         : " (the " + scheme + " sets it only 
with " + METADATA_OPTION.get(scheme)
+                                           + "=true on the endpoint)"));
+                    }
+                }
+            }
+        }
+        return msgs;
+    }
+
     /**
      * The {@link #WRONG_NAMES} the content uses as header names, and the 
names written as a constant that Exchange does
      * not have, which fail the route when Camel creates it; once each, the 
names go into reported.
@@ -290,6 +323,7 @@ final class HeaderChecks {
         while (sm.find()) {
             schemes.add(sm.group(1));
         }
+        msgs.addAll(propertiesReadOnTheBody(content, schemes));
         for (String scheme : schemes) {
             try {
                 var model = catalog.componentModel(scheme);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderPropertyMixups.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderPropertyMixups.java
index 9b49c39627c3..76be04480f4a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderPropertyMixups.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderPropertyMixups.java
@@ -16,17 +16,25 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
+import java.io.StringReader;
 import java.util.ArrayList;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.TreeSet;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import org.apache.camel.dsl.yaml.validator.ProjectFacts;
 import org.yaml.snakeyaml.LoaderOptions;
 import org.yaml.snakeyaml.Yaml;
 import org.yaml.snakeyaml.constructor.SafeConstructor;
+import org.yaml.snakeyaml.nodes.MappingNode;
+import org.yaml.snakeyaml.nodes.Node;
+import org.yaml.snakeyaml.nodes.NodeTuple;
+import org.yaml.snakeyaml.nodes.ScalarNode;
+import org.yaml.snakeyaml.nodes.SequenceNode;
 
 /**
  * A route that keeps a value in one place (a header, an exchange property or 
a variable) and reads it from another gets
@@ -37,6 +45,9 @@ import org.yaml.snakeyaml.constructor.SafeConstructor;
  * Quiet where the name can come from elsewhere: a route that consumes from an 
endpoint that can carry headers (only
  * timer, scheduler, cron, quartz and file cannot), a path parameter {X} of a 
rest or platform-http path, and a name the
  * route sets in both forms.
+ * <p/>
+ * In the same routes, a header copied to itself that nothing sets before 
({@code setHeader: sku} with
+ * {@code ${header.sku}}) is null too, see {@link #selfCopies(String, 
ProjectFacts)}.
  */
 final class HeaderPropertyMixups {
 
@@ -209,6 +220,20 @@ final class HeaderPropertyMixups {
 
     /** The problems, each "Line N: message". */
     static List<String> validate(String content) {
+        return validate(content, ProjectFacts.NONE);
+    }
+
+    /**
+     * The problems, each "Line N: message", with the fields of the JSON data 
of the project for a header copied to
+     * itself.
+     */
+    static List<String> validate(String content, ProjectFacts facts) {
+        List<String> answer = mixups(content);
+        answer.addAll(selfCopies(content, facts));
+        return answer;
+    }
+
+    private static List<String> mixups(String content) {
         List<String> answer = new ArrayList<>();
         if (content == null || !content.contains("${")) {
             return answer;
@@ -343,4 +368,372 @@ final class HeaderPropertyMixups {
         int colon = uri.indexOf(':');
         return colon > 0 ? uri.substring(0, colon) : uri;
     }
+
+    /** ${header.sku}, ${headers.sku}, ${in.header.sku} or ${header[sku]}: one 
header and nothing else. */
+    private static final Pattern ONE_HEADER = Pattern.compile(
+            
"\\$\\{(?:in\\.)?headers?(?:\\.([A-Za-z_][\\w-]*)|\\[['\"]?([A-Za-z_][\\w-]*)['\"]?])}");
+
+    /**
+     * The steps that set no header other than the ones they name, and the 
EIPs whose steps are walked: any other step
+     * (a to:, an enrich, a process, a bean...) may set a header of any name.
+     */
+    private static final Set<String> QUIET_STEPS = Set.of("log", "setBody", 
"transform", "setHeader", "setHeaders",
+            "setProperty", "setVariable", "setVariables", "removeHeader", 
"removeHeaders", "removeProperty",
+            "removeProperties", "removeVariable", "unmarshal", "marshal", 
"convertBodyTo", "convertHeaderTo",
+            "convertVariableTo", "delay", "throttle", "stop", "filter", 
"choice", "split", "loop", "step", "pipeline",
+            "doTry", "idempotentConsumer", "validate", "setExchangePattern");
+
+    /** The languages that run code, which can set a header of any name. */
+    private static final Set<String> SCRIPTS = Set.of("groovy", "js", 
"javascript", "python", "java", "joor", "jsh",
+            "mvel", "ognl", "spel", "jactl", "quickjs", "method", "bean", 
"csimple", "datasonnet", "wasm");
+
+    /** split: ${body[lines]}, ${body.lines} or the jsonpath $.lines: the 
field of the data whose elements it gives. */
+    private static final Pattern SPLIT_FIELD = Pattern.compile(
+            
"\\$\\{body(?:\\??\\.|\\[\\s*['\"]?)([A-Za-z_]\\w*)['\"]?\\s*]?}|\\$\\.([A-Za-z_]\\w*)(?:\\[\\*])?");
+
+    /** What the walk of a route knows at a step: the names set before it, and 
whether a step may set any header. */
+    private static final class Seen {
+        final Set<String> headers = new LinkedHashSet<>();
+        final Set<String> variables = new LinkedHashSet<>();
+        final Set<String> properties = new LinkedHashSet<>();
+        boolean anyHeader;
+
+        Seen copy() {
+            Seen s = new Seen();
+            s.headers.addAll(headers);
+            s.variables.addAll(variables);
+            s.properties.addAll(properties);
+            s.anyHeader = anyHeader;
+            return s;
+        }
+
+        void merge(Seen inner) {
+            headers.addAll(inner.headers);
+            variables.addAll(inner.variables);
+            properties.addAll(inner.properties);
+            anyHeader |= inner.anyHeader;
+        }
+    }
+
+    /**
+     * A setHeader whose value is one header that nothing set before it: the 
line, the name set, the name read, the
+     * field the split it is in gives the elements of, and where the route 
keeps the name read instead (a variable or an
+     * exchange property), or null.
+     */
+    private record UnsetCopy(int line, String name, String read, String split, 
Kind kept) {
+    }
+
+    /**
+     * A setHeader that copies a header to itself, setHeader sku with 
${header.sku}, when nothing sets that header
+     * before it: the route consumes from an endpoint that carries no header 
of its own (timer, scheduler, cron, quartz,
+     * file), and no step before it sets the header. The header is null, and 
stays null, with no error. r23 to r27
+     * connect-http-client step 2 (10 runs): in a split over the lines of an 
order, the HTTP call had no sku, 98 errors
+     * of 404 before the model found ${body[sku]}. Only a header that a later 
step of the route reads, as the toD did. A
+     * step that may set a header the check does not see (a to:, a process, a 
bean, a script) makes it say nothing after
+     * it. A path parameter of a rest in the file is a header of the route the 
rest calls, not of these routes.
+     * <p/>
+     * Only a copy to itself is reported; a copy from another header that 
nothing sets (needed with ${header.qty}) is
+     * named in the same message.
+     */
+    static List<String> selfCopies(String content, ProjectFacts facts) {
+        List<String> answer = new ArrayList<>();
+        if (content == null || !content.contains("header")) {
+            return answer;
+        }
+        List<Node> roots = new ArrayList<>();
+        try {
+            for (Node n : new Yaml(new SafeConstructor(new 
LoaderOptions())).composeAll(new StringReader(content))) {
+                roots.add(n);
+            }
+        } catch (RuntimeException e) {
+            // not YAML: the schema check says so
+            return answer;
+        }
+        String[] lines = content.split("\n", -1);
+        try {
+            for (Node root : roots) {
+                if (!(root instanceof SequenceNode entries)) {
+                    continue;
+                }
+                for (Node entry : entries.getValue()) {
+                    MappingNode route = mapping(value(entry, "route"));
+                    MappingNode from = mapping(route != null ? value(route, 
"from") : value(entry, "from"));
+                    String uri = from != null ? scalar(value(from, "uri")) : 
null;
+                    if (uri == null || !PLAIN_CONSUMERS.contains(scheme(uri))) 
{
+                        continue;
+                    }
+                    List<UnsetCopy> copies = new ArrayList<>();
+                    walk(value(from, "steps"), new Seen(), null, copies);
+                    // only a header that a later step reads: a copy nothing 
reads is harmless (a route that worked had
+                    // one, with the value read from a variable)
+                    int end = from.getEndMark().getLine() + 1;
+                    copies.removeIf(c -> !readAfter(lines, c.line(), end, 
c.name()));
+                    ProjectFacts.JsonFields data = consumedData(uri, from, 
facts);
+                    for (UnsetCopy copy : copies) {
+                        if (copy.name().equals(copy.read())) {
+                            answer.add(selfCopyMessage(copy, copies, data));
+                        }
+                    }
+                }
+            }
+        } catch (RuntimeException e) {
+            // a shape the walk does not know: a check must never fail the 
validation
+        }
+        return answer;
+    }
+
+    /** Whether a line after the given one, up to the end line (1-based, 
inclusive), reads the header in Simple. */
+    private static boolean readAfter(String[] lines, int line, int end, String 
name) {
+        for (int i = line; i < Math.min(end, lines.length); i++) {
+            Matcher m = HEADER_READ.matcher(lines[i]);
+            while (m.find()) {
+                if (name.equals(m.group(1) != null ? m.group(1) : m.group(2))) 
{
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /** Walks the steps in order, with what was set before each one, 
collecting the copies of unset headers. */
+    private static void walk(Node steps, Seen seen, String split, 
List<UnsetCopy> copies) {
+        if (!(steps instanceof SequenceNode list)) {
+            return;
+        }
+        for (Node item : list.getValue()) {
+            if (!(item instanceof MappingNode step) || step.getValue().size() 
!= 1) {
+                seen.anyHeader = true;
+                continue;
+            }
+            String eip = scalar(step.getValue().get(0).getKeyNode());
+            Node body = step.getValue().get(0).getValueNode();
+            if (!QUIET_STEPS.contains(eip) || runsCode(body)) {
+                seen.anyHeader = true;
+                continue;
+            }
+            switch (eip) {
+                case "setHeader" -> setHeader(body, seen, split, copies);
+                case "setHeaders" -> namesOf(value(body, "headers"), 
seen.headers);
+                case "setVariable" -> name(body, seen.variables);
+                case "setVariables" -> namesOf(value(body, "variables"), 
seen.variables);
+                case "setProperty" -> name(body, seen.properties);
+                case "split" -> {
+                    // each element starts from what was set before the split
+                    Seen inner = seen.copy();
+                    walk(value(body, "steps"), inner, split == null ? 
splitField(body) : null, copies);
+                    seen.merge(inner);
+                }
+                case "loop" -> {
+                    // a pass of the loop sees what the passes before it set
+                    Seen first = seen.copy();
+                    walk(value(body, "steps"), first, split, new 
ArrayList<>());
+                    Seen inner = seen.copy();
+                    inner.merge(first);
+                    walk(value(body, "steps"), inner, split, copies);
+                    seen.merge(inner);
+                }
+                default -> nested(body, seen, split, copies);
+            }
+            variableReceive(body, seen);
+        }
+    }
+
+    /** The steps of the EIPs that hold some: when and otherwise of a choice, 
doCatch and doFinally of a doTry. */
+    private static void nested(Node body, Seen seen, String split, 
List<UnsetCopy> copies) {
+        if (!(body instanceof MappingNode map)) {
+            return;
+        }
+        List<Seen> branches = new ArrayList<>();
+        for (NodeTuple t : map.getValue()) {
+            String key = scalar(t.getKeyNode());
+            if ("steps".equals(key)) {
+                Seen inner = seen.copy();
+                walk(t.getValueNode(), inner, split, copies);
+                branches.add(inner);
+            } else if ("when".equals(key) || "doCatch".equals(key)) {
+                if (t.getValueNode() instanceof SequenceNode items) {
+                    for (Node branch : items.getValue()) {
+                        Seen inner = seen.copy();
+                        walk(value(branch, "steps"), inner, split, copies);
+                        branches.add(inner);
+                    }
+                }
+            } else if ("otherwise".equals(key) || "doFinally".equals(key)) {
+                Seen inner = seen.copy();
+                walk(value(t.getValueNode(), "steps"), inner, split, copies);
+                branches.add(inner);
+            }
+        }
+        branches.forEach(seen::merge);
+    }
+
+    private static void setHeader(Node body, Seen seen, String split, 
List<UnsetCopy> copies) {
+        String name = scalar(value(body, "name"));
+        if (name == null) {
+            return;
+        }
+        Node simple = value(body, "simple");
+        if (simple == null && value(body, "expression") instanceof MappingNode 
expression) {
+            simple = value(expression, "simple");
+        }
+        if (simple instanceof MappingNode m) {
+            simple = value(m, "expression");
+        }
+        Matcher m = simple instanceof ScalarNode s ? 
ONE_HEADER.matcher(s.getValue().trim()) : null;
+        if (m != null && m.matches() && !seen.anyHeader) {
+            String read = m.group(1) != null ? m.group(1) : m.group(2);
+            if (!read.startsWith("Camel") && !seen.headers.contains(read)) {
+                Kind kept = seen.variables.contains(read)
+                        ? Kind.VARIABLE : seen.properties.contains(read) ? 
Kind.PROPERTY : null;
+                copies.add(new UnsetCopy(simple.getStartMark().getLine() + 1, 
name, read, split, kept));
+            }
+        }
+        seen.headers.add(name);
+    }
+
+    /** Whether a step runs code of its own, outside the steps it holds: a 
script language can set any header. */
+    private static boolean runsCode(Node node) {
+        if (node instanceof MappingNode map) {
+            for (NodeTuple t : map.getValue()) {
+                String key = scalar(t.getKeyNode());
+                if (SCRIPTS.contains(key)
+                        || "simple".equals(key) && scalar(t.getValueNode()) != 
null
+                                && 
scalar(t.getValueNode()).contains("setHeader(")) {
+                    return true;
+                }
+                if (!"steps".equals(key) && !"when".equals(key) && 
!"otherwise".equals(key) && !"doCatch".equals(key)
+                        && !"doFinally".equals(key) && 
runsCode(t.getValueNode())) {
+                    return true;
+                }
+            }
+        } else if (node instanceof SequenceNode seq) {
+            for (Node n : seq.getValue()) {
+                if (runsCode(n)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /** The field of the data a split gives the elements of, such as lines, or 
null when it is not one. */
+    private static String splitField(Node body) {
+        String text = null;
+        Node expression = value(body, "expression");
+        Node language = expression instanceof MappingNode e ? e : body;
+        for (String key : List.of("simple", "jsonpath")) {
+            Node n = value(language, key);
+            if (n instanceof MappingNode m) {
+                n = value(m, "expression");
+            }
+            if (scalar(n) != null) {
+                text = scalar(n).trim();
+            }
+        }
+        Matcher m = text != null ? SPLIT_FIELD.matcher(text) : null;
+        return m != null && m.matches() ? m.group(1) != null ? m.group(1) : 
m.group(2) : null;
+    }
+
+    /** The JSON data of the files the route consumes, as the project facts 
have them, or null. */
+    private static ProjectFacts.JsonFields consumedData(String uri, 
MappingNode from, ProjectFacts facts) {
+        if (facts == null || !"file".equals(scheme(uri))) {
+            return null;
+        }
+        String directory = uri.length() > "file:".length() ? 
uri.substring("file:".length()) : null;
+        if (directory != null && directory.contains("?")) {
+            directory = directory.substring(0, directory.indexOf('?'));
+        }
+        if (directory == null || directory.isBlank()) {
+            directory = scalar(value(value(from, "parameters"), 
"directoryName"));
+        }
+        return directory != null ? 
facts.jsonFields().get(ProjectFacts.directoryKey(directory)) : null;
+    }
+
+    private static String selfCopyMessage(UnsetCopy copy, List<UnsetCopy> 
copies, ProjectFacts.JsonFields data) {
+        String name = copy.name();
+        StringBuilder sb = new StringBuilder("Line 
").append(copy.line()).append(": ${header.").append(name)
+                .append("} is null here: no step before this line sets the 
header ").append(name)
+                .append(", so setHeader ").append(name).append(" copies null 
to itself. ")
+                .append(copy.kept() != null
+                        ? "The route keeps " + name + " in " + 
copy.kept().what + ": write ${" + copy.kept().read + name
+                          + "}"
+                        : whereTheValueIs(name, copy.split(), data));
+        // the other headers the same route copies before anything sets them, 
such as needed with ${header.qty}
+        for (UnsetCopy other : copies) {
+            if (other != copy && !other.name().equals(other.read())) {
+                sb.append("; the same for 
${header.").append(other.read()).append("} on line ").append(other.line())
+                        .append(": ").append(other.kept() != null
+                                ? "${" + other.kept().read + other.read() + "}"
+                                : readInstead(other.read(), other.split(), 
data));
+            }
+        }
+        return sb.toString();
+    }
+
+    /** What to write for a value the route has not set as a header: a field 
of the split element, or of the data. */
+    private static String whereTheValueIs(String name, String split, 
ProjectFacts.JsonFields data) {
+        Set<String> element = split != null && data != null ? 
data.elementFields().get(split) : null;
+        if (element != null && element.contains(name)) {
+            return "In the split the body is one of the " + split + " of " + 
data.where() + " ("
+                   + String.join(", ", new TreeSet<>(element)) + "): write 
${body[" + name + "]}";
+        }
+        if (element != null && data.fields().contains(name)) {
+            return name + " is a field of the data that was split, not of one 
of the " + split + ": read it before the"
+                   + " split, into a variable (setVariable), and write 
${variable." + name + "} in the split";
+        }
+        if (split != null) {
+            return "In the split the body is the element: a field of it is 
${body[" + name + "]}";
+        }
+        return "Set it from where the value is, such as a field of the body 
(${body[" + name + "]}) or a variable"
+               + " (${variable." + name + "})";
+    }
+
+    /** The read to write instead of a header nothing sets, for the tail of 
the message. */
+    private static String readInstead(String name, String split, 
ProjectFacts.JsonFields data) {
+        Set<String> element = split != null && data != null ? 
data.elementFields().get(split) : null;
+        if (element != null && !element.contains(name) && 
data.fields().contains(name)) {
+            return "${variable." + name + "}, set before the split";
+        }
+        return "${body[" + name + "]}";
+    }
+
+    /** A variableReceive of a step: the variable it sets. */
+    private static void variableReceive(Node body, Seen seen) {
+        String v = scalar(value(body, "variableReceive"));
+        if (v != null) {
+            seen.variables.add(v);
+        }
+    }
+
+    private static void name(Node body, Set<String> into) {
+        String n = scalar(value(body, "name"));
+        if (n != null) {
+            into.add(n);
+        }
+    }
+
+    private static void namesOf(Node items, Set<String> into) {
+        if (items instanceof SequenceNode list) {
+            list.getValue().forEach(item -> name(item, into));
+        }
+    }
+
+    private static Node value(Node node, String key) {
+        if (node instanceof MappingNode map) {
+            for (NodeTuple t : map.getValue()) {
+                if (key.equals(scalar(t.getKeyNode()))) {
+                    return t.getValueNode();
+                }
+            }
+        }
+        return null;
+    }
+
+    private static MappingNode mapping(Node node) {
+        return node instanceof MappingNode m ? m : null;
+    }
+
+    private static String scalar(Node node) {
+        return node instanceof ScalarNode s ? s.getValue() : null;
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LenientRepairs.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LenientRepairs.java
index dfc653aaef76..ee0bfc41ec8d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LenientRepairs.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/LenientRepairs.java
@@ -187,6 +187,11 @@ final class LenientRepairs {
                     line, false, "line " + line + ": " + key + ": the value in 
the case of the enumeration",
                     ls -> enumCase(ls, line - 1, key, choices)));
         } else if ((m = LIST_FOR_MAP.matcher(error)).find()) {
+            if (error.contains("A second condition is another item of when:")) 
{
+                // the items are conditions with their steps, not steps: under 
steps: they are still wrong (r27l-4
+                // connect-http-client step 3, refused 19 times in a row with 
the repair said to need no change)
+                return answer;
+            }
             int line = Integer.parseInt(m.group(1));
             String key = m.group(2);
             answer.add(new Repair(
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
index 6ac170f4752c..d0151de4feb2 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SimpleChecks.java
@@ -18,6 +18,7 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -120,6 +121,15 @@ final class SimpleChecks {
             boolean predicate = !isLogMessage && isPredicate(catalog, lines, 
i, lineIndent);
             checkText(errors, catalog, simpleText, lineNum, predicate, 
isLogMessage,
                     hasAncestorEip(lines, i, lineIndent, "aggregate"));
+            // resolveResource: true loads the resource the result names, so 
there a resource: value is right
+            if (!predicate && !isLogMessage
+                    && !"true".equals(findSiblingValue(lines, textLine, 
countLeadingSpaces(lines[textLine]),
+                            "resolveResource"))) {
+                String resource = resourceAsAValue(simpleText);
+                if (resource != null) {
+                    errors.add("Line " + lineNum + ": " + resource);
+                }
+            }
         }
         // ${property.x}: the hint of the parser, with where the route keeps x
         return HeaderPropertyMixups.withWhereKept(errors, content);
@@ -181,9 +191,77 @@ final class SimpleChecks {
             if (elvis != null) {
                 errors.add("Line " + lineNum + ": " + elvis);
             }
+            String unknown = unknownFunctionWithNested(text, catalog);
+            if (unknown != null) {
+                errors.add("Line " + lineNum + ": " + unknown);
+            }
         }
     }
 
+    /** The name a function starts with: size of ${size:${body}}. */
+    private static final Pattern FUNCTION_NAME = 
Pattern.compile("[A-Za-z][\\w-]*");
+
+    /**
+     * A function whose name Simple does not have, with a function nested in 
it: ${size:${body}}. The parser takes such
+     * a function as a whole only when it evaluates it, so the expression 
passes the check and fails on the first
+     * message with "Unknown function: size:${body}" (r27 rest-code-first step 
1: 4 errors before ${body.size()}). The
+     * catalog is asked with the nested functions as plain text, and only its 
"Unknown function" of that name counts.
+     *
+     * @param  text    the simple text
+     * @param  catalog the catalog that parses it
+     * @return         the message, or null
+     */
+    static String unknownFunctionWithNested(String text, CamelCatalog catalog) 
{
+        if (text == null || catalog == null || !text.contains("${")) {
+            return null;
+        }
+        for (int open = text.indexOf("${"); open >= 0; open = 
text.indexOf("${", open + 2)) {
+            int close = closingBrace(text, open);
+            if (close < 0) {
+                continue;
+            }
+            String inner = text.substring(open + 2, close);
+            int nested = inner.indexOf("${");
+            Matcher name = FUNCTION_NAME.matcher(inner);
+            if (nested <= 0 || !name.lookingAt() || name.end() > nested) {
+                continue;
+            }
+            // ${maskEmail(${header.email})} can call a custom function (a 
SimpleFunction in the registry), which the
+            // catalog does not know: only a name that is not followed by ( is 
never one
+            if (name.end() < inner.length() && inner.charAt(name.end()) == 
'(') {
+                continue;
+            }
+            // the nested functions as a plain value
+            StringBuilder plain = new StringBuilder();
+            int from = 0;
+            for (int n = inner.indexOf("${"); n >= 0; n = inner.indexOf("${", 
from)) {
+                int end = closingBrace(inner, n);
+                if (end < 0) {
+                    break;
+                }
+                plain.append(inner, from, n).append('x');
+                from = end + 1;
+            }
+            plain.append(inner.substring(from));
+            String error;
+            try {
+                LanguageValidationResult result = 
catalog.validateLanguageExpression(null, "simple", "${" + plain + "}");
+                error = result.isSuccess() ? null : result.getShortError() != 
null ? result.getShortError() : result.getError();
+            } catch (Exception e) {
+                error = null;
+            }
+            if (error == null || !error.startsWith("Unknown function: " + 
name.group())) {
+                continue;
+            }
+            String function = text.substring(open, close + 1);
+            String written = name.group() + (name.end() < inner.length() ? 
String.valueOf(inner.charAt(name.end())) : "");
+            boolean size = Set.of("size", "count", 
"length").contains(name.group());
+            return "Simple has no function " + written + ", so " + function + 
" fails when the route runs (Unknown"
+                   + " function: " + inner + ")" + (size ? ": the number of 
elements of the body is ${body.size()}" : "");
+        }
+        return null;
+    }
+
     /** The functions whose text in parentheses is another language: 
${jsonpath($[?(@.sku == 'A')])} is right. */
     private static final Pattern OTHER_LANGUAGE = 
Pattern.compile("(jsonpath|jq|xpath)\\s*\\(");
 
@@ -215,6 +293,10 @@ final class SimpleChecks {
      * <tt>${bodyAs(List).?[orderId == '${header.id}'].size()}</tt> failed 
with "ClassNotFoundException: List" and
      * "Method with name: ? not found", and <tt>${body[?(@.orderId == 
'${header.id}')].size()}</tt> with "Key
      * ?(@.orderId == 'ORD-1001') not found". The message gives the jsonpath 
step that does it, as YAML lines.
+     * <p/>
+     * The same for a filter written right after the function, 
<tt>${body}?.?[orderId == '${header.id}']</tt> (r27
+     * rest-code-first step 1): text there, which the parser refused in a 
predicate with "operators and values must be
+     * separated by a space", and took as literal text in an expression.
      *
      * @param  text the simple text
      * @return      the message, or null when no function has such a filter
@@ -230,9 +312,18 @@ final class SimpleChecks {
                 continue;
             }
             String inner = text.substring(open + 2, close);
-            int at = OTHER_LANGUAGE.matcher(inner).lookingAt() ? -1 : 
filterIndex(inner);
+            if (OTHER_LANGUAGE.matcher(inner).lookingAt()) {
+                continue;
+            }
+            int at = filterIndex(inner);
             if (at >= 0) {
-                return filterMessage(inner, at);
+                return filterMessage(inner, at, null);
+            }
+            // a filter right after the closing brace, after a null-safe ?. or 
not
+            String after = text.substring(close + 1);
+            after = after.startsWith("?.") ? after.substring(1) : after;
+            if (filterIndex(after) == 0) {
+                return filterMessage(inner + after, inner.length(), 
text.substring(open, close + 1));
             }
         }
         return null;
@@ -267,14 +358,21 @@ final class SimpleChecks {
         return -1;
     }
 
-    /** The message for a filter at the index of the text of a function, with 
the jsonpath step that picks it. */
-    private static String filterMessage(String inner, int at) {
+    /**
+     * The message for a filter at the index of the text of a function, with 
the jsonpath step that picks it; the
+     * function is the one the filter was written after, or null when it is 
inside it.
+     */
+    private static String filterMessage(String inner, int at, String function) 
{
         boolean spel = inner.charAt(at) == '.';
         String written = spel ? inner.substring(at, at + 3) + "...]" : 
inner.startsWith("@.", at) ? "@." : "[?(...)]";
         StringBuilder sb = new StringBuilder("Simple has no ")
                 .append(spel && inner.charAt(at + 1) == '!' ? "projection" : 
"filter").append(": ").append(written)
-                .append(spel ? " is SpEL" : " is JsonPath").append(", which 
Simple reads as a ")
-                .append(spel ? "method" : "key").append(", so it fails at 
runtime. ");
+                .append(spel ? " is SpEL" : " is JsonPath");
+        if (function != null) {
+            sb.append(", and after ").append(function).append(" it is text, 
not a part of the function. ");
+        } else {
+            sb.append(", which Simple reads as a ").append(spel ? "method" : 
"key").append(", so it fails at runtime. ");
+        }
         // the path to the List the filter is on, and where it is when not in 
the body
         String root = "$";
         String source = null;
@@ -482,6 +580,16 @@ final class SimpleChecks {
         // the expression itself, rewritten: with only an example, models 
wrote the same top-level ternary up to 20
         // times in a row (a step of the local-model benchmark)
         String rewritten = ternaryInsideOneFunction(text, question, colon);
+        if (rewritten != null && rewritten.startsWith("resource:") && 
!text.startsWith("resource:")) {
+            // the resource: of the values went before the function: say why, 
or the model writes it inside again (r27
+            // of the local-model benchmark: the body was the text 
resource:file:order.json, then a JsonParseException)
+            String scheme = rewritten.substring(0, rewritten.indexOf("${"));
+            return "Simple has no top-level ternary: the ? at index " + 
(question + 1)
+                   + " and its : are outside ${...}, so they are literal text 
and the operator is never evaluated."
+                   + " Write the whole ternary inside one function, and " + 
scheme + " before it: " + rewritten
+                   + " (a function's result is never loaded as a resource: 
with " + scheme + " inside the function,"
+                   + " the body is the text " + 
unquoted(text.substring(question + 3, colon).trim()) + ", not the file)";
+        }
         return "Simple has no top-level ternary: the ? at index " + (question 
+ 1)
                + " and its : are outside ${...}, so they are literal text and 
the operator is never evaluated."
                + (rewritten != null
@@ -501,7 +609,7 @@ final class SimpleChecks {
      * order-bad-email.json</tt> is 
<tt>resource:file:${exchangeProperty.CamelTimerCounter == 1 ? 'order.json' :
      * 'order-bad-email.json'}</tt>, and so is
      * <tt>${exchangeProperty.CamelTimerCounter} == 1 ? 
resource:file:order.json :
-     * resource:file:order-bad-email.json</tt>.
+     * resource:file:order-bad-email.json</tt>, with the values quoted or not.
      */
     static String ternaryInsideOneFunction(String text, int question, int 
colon) {
         // the functions at the top level before the ?, as {start, end} with 
end the index of the closing brace
@@ -534,20 +642,110 @@ final class SimpleChecks {
         String second = text.substring(colon + 3).trim();
         // resource: is read only at the start of the expression: inside the 
function, 'resource:file:order.json' is the
         // text the body becomes, so a resource: both values start with goes 
before the function (the local-model
-        // benchmark: the body was the text resource:file:order.json, and the 
next step failed to parse it as JSON)
-        Matcher scheme = RESOURCE_SCHEME.matcher(first);
-        if (prefix.isEmpty() && scheme.lookingAt() && 
second.startsWith(scheme.group())
-                && first.length() > scheme.end() && second.length() > 
scheme.end()) {
+        // benchmark: the body was the text resource:file:order.json, and the 
next step failed to parse it as JSON).
+        // The values may be quoted already, as r27 wrote them: 
'resource:file:order.json'
+        String firstText = unquoted(first);
+        String secondText = unquoted(second);
+        Matcher scheme = RESOURCE_SCHEME.matcher(firstText);
+        if (prefix.isEmpty() && scheme.lookingAt() && 
secondText.startsWith(scheme.group())
+                && firstText.length() > scheme.end() && secondText.length() > 
scheme.end()) {
             prefix = scheme.group();
-            first = first.substring(scheme.end());
-            second = second.substring(scheme.end());
+            first = firstText.substring(scheme.end());
+            second = secondText.substring(scheme.end());
         }
         return prefix + "${" + condition + " ? " + quoted(first) + " : " + 
quoted(second) + "}";
     }
 
+    /** A value of the ternary without the quotes of a quoted literal: 
'order.json' is order.json. */
+    private static String unquoted(String value) {
+        if (value.length() > 1 && (value.startsWith("'") && value.endsWith("'")
+                || value.startsWith("\"") && value.endsWith("\""))) {
+            return value.substring(1, value.length() - 1);
+        }
+        return value;
+    }
+
     /** The resource: and its scheme a value starts with, such as 
<tt>resource:file:</tt>. */
     private static final Pattern RESOURCE_SCHEME = 
Pattern.compile("resource:[a-z]+:");
 
+    /** A quoted literal of a resource: and its scheme, as a whole value: 
'resource:file:order.json'. */
+    private static final Pattern QUOTED_RESOURCE = 
Pattern.compile("(['\"])(resource:[a-z]+:)(.+)\\1");
+
+    /**
+     * A resource: written as a value a function gives: a value of a ternary 
or an elvis inside ${...}. The value is the
+     * text, such as resource:file:order.json, and nothing loads it: resource: 
is read only at the start of the
+     * expression. r27 of the local-model benchmark wrote
+     * <tt>${exchangeProperty.CamelTimerCounter == 1 ? 
'resource:file:order.json' : 'resource:file:order-bad-email.json'}</tt>,
+     * the write was accepted, and the next step failed to parse that text as 
JSON (12 errors in a run). Not said for an
+     * expression that starts with resource:, whose location is loaded, nor 
for an elvis after a function, which a route
+     * that worked had as a step whose body the next step replaced.
+     *
+     * @param  text the simple text, used as an expression
+     * @return      the message, or null when no value is a resource:
+     */
+    static String resourceAsAValue(String text) {
+        if (text == null || text.startsWith("resource:") || 
!text.contains("resource:") || !text.contains("${")) {
+            return null;
+        }
+        // the values of a ternary and of an elvis inside each function, the 
nested ones too
+        for (int open = text.indexOf("${"); open >= 0; open = 
text.indexOf("${", open + 2)) {
+            int close = closingBrace(text, open);
+            if (close < 0) {
+                continue;
+            }
+            String inner = text.substring(open + 2, close);
+            List<String> values = new ArrayList<>();
+            int question = indexOfTopLevel(inner, " ? ", 0);
+            int colon = question < 0 ? -1 : indexOfTopLevel(inner, " : ", 
question + 3);
+            if (colon > 0) {
+                values.add(inner.substring(question + 3, colon).trim());
+                values.add(inner.substring(colon + 3).trim());
+            }
+            int elvis = indexOfTopLevel(inner, " ?: ", 0);
+            if (elvis > 0) {
+                values.add(inner.substring(elvis + 4).trim());
+            }
+            // the condition, for the rewrite: only when the function is the 
whole expression
+            String condition = colon > 0 && open == 0 && close == 
text.length() - 1 ? inner.substring(0, question) : null;
+            String message = resourceValueMessage(text.substring(open, close + 
1), values, condition);
+            if (message != null) {
+                return message;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * The message for the values of an expression of which one is a quoted 
resource:, with resource: before the
+     * function when both values of the ternary of the condition are of the 
same scheme; null when none is a resource:.
+     */
+    private static String resourceValueMessage(String written, List<String> 
values, String condition) {
+        List<Matcher> resources = new ArrayList<>();
+        for (String value : values) {
+            Matcher m = QUOTED_RESOURCE.matcher(value);
+            if (m.matches()) {
+                resources.add(m);
+            }
+        }
+        if (resources.isEmpty()) {
+            return null;
+        }
+        String scheme = resources.get(0).group(2);
+        StringBuilder sb = new StringBuilder("`").append(written).append("` 
gives the text ").append(scheme)
+                .append(resources.get(0).group(3)).append(", not the file: a 
function's result is never loaded as a")
+                .append(" resource, only a resource: at the start of the 
expression is.");
+        if (condition != null && values.size() == 2 && resources.size() == 2
+                && scheme.equals(resources.get(1).group(2))) {
+            sb.append(" Write ").append(scheme).append(" before the function: 
").append(scheme).append("${")
+                    .append(condition).append(" ? 
").append(quoted(resources.get(0).group(3))).append(" : ")
+                    .append(quoted(resources.get(1).group(3))).append("}");
+        } else {
+            sb.append(" Write ").append(scheme).append(" at the start of the 
expression, and a function only for the")
+                    .append(" part of the name that changes, as in 
").append(scheme).append("order-${header.id}.json");
+        }
+        return sb.toString();
+    }
+
     /** The index of the brace that closes the function opened at the index, 
or -1. */
     private static int closingBrace(String text, int open) {
         char quote = 0;
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
index 4b74bee533f9..135c7ccf35f1 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidator.java
@@ -151,8 +151,8 @@ public final class SourceValidator {
             // route: read it, so that a GET operation is known to carry no 
body (CAMEL-24844), and with the REST binding
             // mode of the properties, whether a Map sent or returned over 
REST is written for the route (CAMEL-25762);
             // and what a running integration of the file saw at the endpoints 
of its routes (CAMEL-25750)
-            List<String> msgs = validateCamelYaml(content, catalog, 
schemaValidator,
-                    ValidatorFacts.read(fileName, content, directory));
+            ProjectFacts facts = ValidatorFacts.read(fileName, content, 
directory);
+            List<String> msgs = validateCamelYaml(content, catalog, 
schemaValidator, facts);
             if (name.endsWith(".kamelet.yaml") || 
name.endsWith(".kamelet.yml")) {
                 // the shape of a Kamelet file, which the schema of a route 
file does not know
                 List<String> shape = 
KameletChecks.validateKameletFile(content);
@@ -183,8 +183,9 @@ public final class SourceValidator {
                 }
             }
             if (msgs.isEmpty()) {
-                // a header read where the route keeps the name in an exchange 
property, or the reverse (CAMEL-25516)
-                msgs = new ArrayList<>(HeaderPropertyMixups.validate(content));
+                // a header read where the route keeps the name in an exchange 
property, or the reverse (CAMEL-25516),
+                // and a header copied to itself that nothing sets
+                msgs = new ArrayList<>(HeaderPropertyMixups.validate(content, 
facts));
             }
             // an option at the column of its EIP: the lines to write, which a 
model copies (the words it does not,
             // CAMEL-25556)
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsLenientTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsLenientTest.java
index 190087096e95..dadfcf4bc29a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsLenientTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsLenientTest.java
@@ -362,6 +362,73 @@ class AuthoringToolsLenientTest {
                 .contains("the write repairs it by itself");
     }
 
+    /**
+     * r27l-4 connect-http-client step 3: a second condition and an - 
otherwise: item written under otherwise:. The
+     * write put them under steps:, where they were still not steps, and said 
the shape needed no change; the model sent
+     * the same lines 16 times. They are conditions: no repair, and the answer 
says where they go.
+     */
+    @Test
+    void anOtherwiseHoldingASecondConditionIsNotRepaired(@TempDir Path dir) {
+        JsonObject result = write(dir, """
+                - route:
+                    from:
+                      uri: direct:check
+                      steps:
+                        - choice:
+                            when:
+                              - simple:
+                                  expression: "${header.CamelHttpResponseCode} 
!= 200"
+                                steps:
+                                  - log: "not found"
+                            otherwise:
+                              - simple:
+                                  expression: "${body[qty]} >= 
${variable.needed}"
+                                steps:
+                                  - log: "in stock"
+                              - otherwise:
+                                steps:
+                                  - log: "back-order"
+                """);
+        
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("invalid");
+        assertThat(result.get("repaired")).isNull();
+        assertThat(result.get("repairable")).isNull();
+        assertThat(result.getString("message")).doesNotContain("needs no 
change from you");
+        assertThat(result.get("errors").toString()).contains("Line 12: 
otherwise: array found, object expected (otherwise"
+                                                             + " holds only 
steps:, the steps for a message that no when"
+                                                             + " matches. A 
second condition is another item of when:");
+    }
+
+    /**
+     * r27n-2 contracts-rest-code-first step 1: - otherwise: written at the 
column of when:, which the write moves into
+     * the when: list. There it is still not an item of when, so the note must 
not say that the shape needs no change.
+     */
+    @Test
+    void aRepairWhoseLinesStillHaveAProblemOfTheirShapeIsSaidSo(@TempDir Path 
dir) {
+        JsonObject result = write(dir, """
+                - route:
+                    from:
+                      uri: direct:get-order
+                      steps:
+                        - choice:
+                            when:
+                              - simple: "${body} != null"
+                                steps:
+                                  - log: "found"
+                            - otherwise:
+                                steps:
+                                  - log: "not found"
+                """);
+        
assertThat(result.getString("status")).as(String.valueOf(result)).isEqualTo("invalid");
+        assertThat(result.get("errors").toString()).contains("otherwise is not 
an item of when: it is a key of choice");
+        assertThat(result.get("repairable").toString())
+                .contains("line 10: the list item moved to the column of the 
list (these lines still have errors)");
+        assertThat(result.getString("message")).doesNotContain("needs no 
change from you")
+                .endsWith("The write repairs part of the YAML shape by itself 
(line 10: the list item moved to the column"
+                          + " of the list (these lines still have errors)), 
and the lines marked still have errors of"
+                          + " their shape: change them as the errors say. The 
errors are what the content has with that"
+                          + " repaired.");
+    }
+
     @Test
     void aProblemTheWriteCannotRepairKeepsTheAnswerAsItWas(@TempDir Path dir) {
         // the case of library: is repaired, the misspelt key is not: both are 
said as written, nothing is hidden
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
index 6388f2f87bad..f2c851c514e7 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
@@ -44,6 +44,10 @@ class ExpressionEvaluatorLanguageTest {
                 .contains("leave the index out, as in $[?(@.sku == 
'CAMEL-MUG')].qty");
         assertThat(ExpressionEvaluator.filterNote("jsonpath", "$.sku", 
"ok")).isNull();
         assertThat(ExpressionEvaluator.filterNote("simple", "${body}", 
"ok")).isNull();
+        // a SpEL filter in Simple, written after the function (r27 
rest-code-first step 1, evaluated twice)
+        assertThat(ExpressionEvaluator.filterNote("simple", 
"${body}?.?[orderId == 'ORD-1001']", "error"))
+                .startsWith("Simple has no filter: .?[...] is SpEL, and after 
${body} it is text")
+                .contains("jsonpath:\n        expression: \"$[?(@.orderId == 
'ORD-1001')]\"");
         assertThat(ExpressionEvaluator.filterNote("jsonpath", "$[?(@.sku == 
'X')", "error")).isNull();
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderPropertyMixupsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderPropertyMixupsTest.java
index 2ba9804ead94..fc358cca485d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderPropertyMixupsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderPropertyMixupsTest.java
@@ -16,15 +16,20 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
 import java.util.List;
 
 import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 import static org.assertj.core.api.Assertions.assertThat;
 
 /**
- * CAMEL-25516: a header, exchange property or variable read where the route 
keeps the name in another of them.
+ * CAMEL-25516: a header, exchange property or variable read where the route 
keeps the name in another of them; and a
+ * header copied to itself that nothing sets.
  */
 class HeaderPropertyMixupsTest {
 
@@ -193,4 +198,149 @@ class HeaderPropertyMixupsTest {
                         m -> assertThat(m).endsWith(
                                 "Unknown function: property.CamelSplitIndex 
(did you mean ${exchangeProperty.CamelSplitIndex}?)"));
     }
+
+    /**
+     * r27l-4 connect-http-client step 2 (shortened): in the split over the 
lines of an order, sku and needed copied
+     * from headers that nothing sets. The call went to 
http://localhost:8080/stock/ with no sku: 98 errors of 404.
+     */
+    private static final String CHECK_STOCK = """
+            - route:
+                id: check-stock
+                from:
+                  uri: file
+                  parameters:
+                    directoryName: orders
+                    noop: true
+                  steps:
+                    - unmarshal:
+                        json:
+                          library: Jackson
+                    - setVariable:
+                        name: orderId
+                        expression:
+                          simple:
+                            expression: "${body[orderId]}"
+                    - split:
+                        expression:
+                          simple:
+                            expression: "${body[lines]}"
+                        steps:
+                          - setHeader:
+                              name: sku
+                              expression:
+                                simple:
+                                  expression: "${header.sku}"
+                          - setHeader:
+                              name: needed
+                              expression:
+                                simple:
+                                  expression: "${header.qty}"
+                          - setBody:
+                              expression:
+                                simple:
+                                  expression: "${null}"
+                          - toD:
+                              uri: "http://localhost:8080/stock/${header.sku}";
+                          - unmarshal:
+                              json:
+                                library: Jackson
+                          - log:
+                              message: "${variable.orderId}: ${header.sku} x 
${header.needed}, ${body[qty]} in stock"
+            """;
+
+    /** The rest of connect-http-client, whose path parameter is a header of 
the stock service route. */
+    private static final String REST = """
+            - rest:
+                path: /stock
+                get:
+                  - path: "/{sku}"
+                    to:
+                      uri: direct:one-sku
+            """;
+
+    @Test
+    void aHeaderCopiedToItselfThatNothingSetsIsReported() {
+        // the stock service of the same file has the path parameter {sku}: a 
header of the route the rest calls only
+        assertThat(HeaderPropertyMixups.validate(CHECK_STOCK + 
REST)).singleElement().asString()
+                .startsWith("Line 26: ${header.sku} is null here");
+        
assertThat(HeaderPropertyMixups.validate(CHECK_STOCK)).singleElement().isEqualTo(
+                "Line 26: ${header.sku} is null here: no step before this line 
sets the header sku, so setHeader sku"
+                                                                               
          + " copies null to itself. In the split the body is the element: a 
field of it"
+                                                                               
          + " is ${body[sku]}; the same for ${header.qty} on line 31: 
${body[qty]}");
+    }
+
+    @Test
+    void theFieldsOfTheDataFilesSayWhatToRead(@TempDir Path dir) throws 
IOException {
+        Files.createDirectories(dir.resolve("orders"));
+        Files.writeString(dir.resolve("orders/order-1001.json"), """
+                {"orderId": "ORD-1001", "customer": "C-482", "lines": [{"sku": 
"CAMEL-TSHIRT", "qty": 2, "price": 19.9}]}
+                """);
+        List<String> msgs = SourceValidator.validate("http-client.camel.yaml", 
CHECK_STOCK, new DefaultCamelCatalog(),
+                null, dir);
+        assertThat(msgs).singleElement().isEqualTo(
+                "Line 26: ${header.sku} is null here: no step before this line 
sets the header sku, so setHeader sku"
+                                                   + " copies null to itself. 
In the split the body is one of the lines of the"
+                                                   + " files in orders/ 
(price, qty, sku): write ${body[sku]}; the same for"
+                                                   + " ${header.qty} on line 
31: ${body[qty]}");
+        // r23l-3: the order id copied to itself in the split, and no variable 
for it: a field of the order, not of a line
+        String orderId = CHECK_STOCK
+                .replace("        - setVariable:\n            name: orderId\n  
          expression:\n              simple:\n"
+                         + "                expression: 
\"${body[orderId]}\"\n",
+                        "")
+                .replace("name: sku\n                  expression:\n           
         simple:\n"
+                         + "                      expression: 
\"${header.sku}\"",
+                        "name: orderId\n                  expression:\n        
            simple:\n"
+                                                                               
   + "                      expression: \"${header.orderId}\"")
+                .replace("${variable.orderId}: ", "${header.orderId}: ");
+        assertThat(SourceValidator.validate("http-client.camel.yaml", orderId, 
new DefaultCamelCatalog(), null, dir))
+                .singleElement().asString()
+                .startsWith("Line 21: ${header.orderId} is null here: no step 
before this line sets the header orderId")
+                .contains("orderId is a field of the data that was split, not 
of one of the lines: read it before the"
+                          + " split, into a variable (setVariable), and write 
${variable.orderId} in the split");
+    }
+
+    @Test
+    void aHeaderCopiedToItselfIsNotReportedWhereItCanBeSet() {
+        String split = "        - split:\n";
+        // set before: no copy to itself is left, and a copy from another 
header alone is not reported
+        assertThat(HeaderPropertyMixups.validate(CHECK_STOCK.replace(split,
+                "        - setHeader:\n            name: sku\n            
simple: \"${body[lines][0][sku]}\"\n" + split)))
+                .isEmpty();
+        // a step that may set any header before it: a to:, an enrich, a 
process, a script
+        for (String step : List.of("- to: direct:prepare", "- enrich:\n    
expression:\n      constant: direct:prepare",
+                "- process:\n    ref: prepare", "- setBody:\n    groovy: 
\"headers.sku = 'A'; body\"")) {
+            
assertThat(HeaderPropertyMixups.validate(CHECK_STOCK.replace(split, 
step.indent(8) + split))).as(step)
+                    .isEmpty();
+        }
+        // a consumer that can carry the header, or a caller that can set it
+        for (String from : List.of("direct", "seda", "platform-http", "jms", 
"kafka")) {
+            assertThat(HeaderPropertyMixups.validate(CHECK_STOCK.replace("uri: 
file", "uri: " + from))).as(from)
+                    .isEmpty();
+        }
+
+        // r27l-6, a passed step: the order id copied to itself, and the log 
reads the variable: the null is never read
+        assertThat(HeaderPropertyMixups.validate(CHECK_STOCK.replace("name: 
sku", "name: orderId")
+                .replace("expression: \"${header.sku}\"", "expression: 
\"${header.orderId}\""))).isEmpty();
+        // a header of Camel itself, which the consumer sets
+        assertThat(HeaderPropertyMixups.validate(CHECK_STOCK.replace("name: 
sku", "name: CamelFileName")
+                .replace("expression: \"${header.sku}\"", "expression: 
\"${header.CamelFileName}\""))).isEmpty();
+    }
+
+    @Test
+    void aHeaderCopiedToItselfThatTheRouteKeepsInAVariable() {
+        String copy = "            steps:\n              - setHeader:\n        
          name: sku\n";
+        String variable
+                = "              - setVariable:\n                  name: sku\n 
                 simple: \"${body[sku]}\"\n";
+        assertThat(HeaderPropertyMixups.validate(CHECK_STOCK.replace(copy,
+                "            steps:\n" + variable + "              - 
setHeader:\n                  name: sku\n")))
+                .singleElement().asString()
+                .startsWith("Line 29: ${header.sku} is null here: no step 
before this line sets the header sku")
+                .contains("The route keeps sku in a variable: write 
${variable.sku}");
+        // a variable set after the copy is not what the copy reads
+        assertThat(HeaderPropertyMixups
+                .validate(CHECK_STOCK.replace("              - setHeader:\n    
              name: needed\n",
+                        variable + "              - setHeader:\n               
   name: needed\n")))
+                .singleElement().asString()
+                .contains("In the split the body is the element: a field of it 
is ${body[sku]}");
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
index f66e165a0690..61b5a6f5f4f7 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
@@ -408,6 +408,72 @@ class SourceValidatorEndpointTest {
         assertThat(QuickFixes.fixFor(msgs.get(0), "    uri: 
cron:cron?cron=0/10 * * * * ?")).isNull();
     }
 
+    /**
+     * r27n-5 connect-service-jms-request-reply step 2: told to keep the 
message with variableReceive, the model put it
+     * under parameters: of the to:, and pattern before it. Both are keys of 
the to:, next to uri:.
+     */
+    @Test
+    void anOptionOfTheEipUnderParametersSaysItIsAKeyOfTheEip() {
+        List<String> msgs = SourceValidator.validateYamlEndpoints("""
+                - route:
+                    id: reserve-order
+                    from:
+                      uri: jms
+                      parameters:
+                        destinationType: queue
+                        destinationName: orders
+                        variableReceive: order
+                      steps:
+                        - to:
+                            uri: jms
+                            parameters:
+                              destinationType: queue
+                              destinationName: stock
+                              exchangePattern: InOut
+                              variableReceive: stockReply
+                        - to:
+                            uri: jms
+                            parameters:
+                              destinationName: stock
+                              pattern: InOut
+                        - to: "jms:queue:stock?variableReceive=stockReply"
+                """, catalog);
+        assertThat(msgs).hasSize(4);
+        assertThat(msgs.get(0)).isEqualTo("Line 8: jms: Unknown option 
'variableReceive' (variableReceive is a key of the"
+                                          + " from:, next to uri:, not an 
option of the jms endpoint: from: {uri: jms,"
+                                          + " parameters: {...}, 
variableReceive: order})");
+        assertThat(msgs.get(1)).isEqualTo("Line 16: jms: Unknown option 
'variableReceive' (variableReceive is a key of"
+                                          + " the to:, next to uri:, not an 
option of the jms endpoint: to: {uri: jms,"
+                                          + " parameters: {...}, 
variableReceive: stockReply})");
+        assertThat(msgs.get(2)).isEqualTo("Line 21: jms: Unknown option 
'pattern' (pattern is a key of the to:, next to"
+                                          + " uri:, not an option of the jms 
endpoint: to: {uri: jms, parameters: {...},"
+                                          + " pattern: InOut})");
+        assertThat(msgs.get(3)).isEqualTo("Line 22: jms: Unknown option 
'variableReceive' (variableReceive is a key of"
+                                          + " the to:, next to uri:, not an 
option of the jms endpoint: to: {uri:"
+                                          + " \"jms:queue:stock\", 
variableReceive: stockReply})");
+        // the keys where they go, and an option that is neither the 
endpoint's nor the EIP's
+        assertThat(SourceValidator.validateYamlEndpoints("""
+                - from:
+                    uri: timer:tick
+                    steps:
+                      - to:
+                          uri: jms
+                          parameters:
+                            destinationName: stock
+                          pattern: InOut
+                          variableReceive: stockReply
+                      - to:
+                          uri: jms
+                          parameters:
+                            destinatonName: stock
+                """, catalog)).anySatisfy(m -> assertThat(m).contains("Unknown 
option 'destinatonName'. Did you mean"))
+                .noneMatch(m -> m.contains("is a key of"));
+        // the Java and XML DSLs write the options in the uri
+        assertThat(EndpointChecks.optionOfTheEip(catalog, "to", 
"variableReceive", "jms",
+                "jms:queue:stock?variableReceive=stockReply", true, false))
+                .isEqualTo("variableReceive is an option of the to, not of the 
jms endpoint: set it on the to, not in the uri");
+    }
+
     @Test
     void aWildcardInTheFileIncludeOptionIsNamedAsNotARegex() {
         List<String> msgs = SourceValidator.validateYamlEndpoints("""
@@ -987,6 +1053,41 @@ class SourceValidatorEndpointTest {
         assertThat(msgs).isEmpty();
     }
 
+    /** r27l-4 transform-groovy step 3: 
resource:file:order-${body[CamelTimerCounter]}.json loaded order-.json. */
+    @Test
+    void aTimerExchangePropertyReadOnTheBodyIsReported() {
+        List<String> msgs = SourceValidator.validateKnownHeaders("""
+                - from:
+                    uri: timer:orders?repeatCount=2&includeMetadata=true
+                    steps:
+                      - setBody:
+                          simple: 
"resource:file:order-${body[CamelTimerCounter]}.json"
+                      - log: "${body.CamelTimerName} 
${body[CamelTimerCounter]}"
+                """, catalog);
+        assertThat(msgs).hasSize(2);
+        assertThat(msgs.get(0)).isEqualTo("Line 5: ${body[CamelTimerCounter]} 
reads a field of the body, and the body has"
+                                          + " no field CamelTimerCounter: 
CamelTimerCounter is an exchange property of the"
+                                          + " timer, write 
${exchangeProperty.CamelTimerCounter}");
+        assertThat(msgs.get(1)).startsWith("Line 6: ${body.CamelTimerName} 
reads a field of the body");
+        // without includeMetadata the timer does not set it, which the 
message says too
+        assertThat(SourceValidator.validateKnownHeaders("""
+                - from:
+                    uri: timer:orders?repeatCount=2
+                    steps:
+                      - setBody:
+                          simple: 
"resource:file:order-${body.CamelTimerCounter}.json"
+                """, catalog)).singleElement().asString()
+                .endsWith("write ${exchangeProperty.CamelTimerCounter} (the 
timer sets it only with includeMetadata=true"
+                          + " on the endpoint)");
+        // a field of the body with a name of Camel that no component of the 
route sets as an exchange property
+        assertThat(SourceValidator.validateKnownHeaders("""
+                - from:
+                    uri: file:orders
+                    steps:
+                      - log: "${body[CamelTimerCounter]}"
+                """, catalog)).isEmpty();
+    }
+
     @Test
     void withIncludeMetadataTheHeadersMapStillHasNoSuchHeader() {
         List<String> msgs = SourceValidator.validateKnownHeaders("""
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
index ca1debe94fc6..d45408fd730b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSimpleTest.java
@@ -481,8 +481,12 @@ class SourceValidatorSimpleTest {
 
     private static String rewritten(String text) {
         String message = SimpleChecks.topLevelTernary(text);
-        assertThat(message).contains("Write the whole ternary inside one 
function: ");
-        return message.substring(message.indexOf("inside one function: ") + 
"inside one function: ".length());
+        assertThat(message).containsPattern("Write the whole ternary inside 
one function(, and resource:\\w+: before it)?: ");
+        String start = message.contains(" before it: ") ? " before it: " : 
"inside one function: ";
+        String rewrite = message.substring(message.indexOf(start) + 
start.length());
+        // the reason a resource: goes before the function follows the rewrite
+        int reason = rewrite.indexOf(" (a function's result is never loaded");
+        return reason > 0 ? rewrite.substring(0, reason) : rewrite;
     }
 
     @Test
@@ -514,6 +518,107 @@ class SourceValidatorSimpleTest {
                 .isEqualTo("${header.a == 1 ? 'resource:file:order.json' : 
'none'}");
     }
 
+    /**
+     * r27 transform-groovy step 3: the values quoted, as two runs wrote them. 
The hint kept them inside the function,
+     * so the body was the text resource:file:order.json, and the next step 
failed with a JsonParseException (12
+     * errors).
+     */
+    @Test
+    void 
aResourceInBothQuotedValuesGoesBeforeTheFunctionAndTheMessageSaysWhy() {
+        String message = SimpleChecks.topLevelTernary(
+                "${exchangeProperty.CamelTimerCounter} == 0 ? 
'resource:file:order.json' : 'resource:file:order-bad-email.json'");
+        assertThat(message).contains("Write the whole ternary inside one 
function, and resource:file: before it:"
+                                     + " 
resource:file:${exchangeProperty.CamelTimerCounter == 0 ? 'order.json' :"
+                                     + " 'order-bad-email.json'} (a function's 
result is never loaded as a resource:"
+                                     + " with resource:file: inside the 
function, the body is the text"
+                                     + " resource:file:order.json, not the 
file)");
+        // r27l-4 wrote the timer's counter as a field of the body, and double 
quotes
+        assertThat(rewritten(
+                "${body[CamelTimerCounter]} == 1 ? 
\"resource:file:order.json\" : \"resource:file:order-bad-email.json\""))
+                .isEqualTo("resource:file:${body[CamelTimerCounter] == 1 ? 
'order.json' : 'order-bad-email.json'}");
+        // the resource: before the condition already: as before, with no 
reason to give
+        assertThat(SimpleChecks.topLevelTernary(
+                "resource:file:${exchangeProperty.CamelTimerCounter} == 0 ? 
'order.json' : 'order-bad-email.json'"))
+                .endsWith("Write the whole ternary inside one function:"
+                          + " 
resource:file:${exchangeProperty.CamelTimerCounter == 0 ? 'order.json' : 
'order-bad-email.json'}");
+        // quoted values of two schemes stay as they are
+        assertThat(rewritten("${header.a} == 1 ? 'resource:file:order.json' : 
'resource:classpath:order.json'"))
+                .isEqualTo("${header.a == 1 ? 'resource:file:order.json' : 
'resource:classpath:order.json'}");
+    }
+
+    /**
+     * r27 transform-groovy step 3: the rewrite of the old hint, written as 
told, passed the write and failed at
+     * runtime. The value is the text, which nothing loads; the same for the 
value of an elvis inside the function.
+     */
+    @Test
+    void aResourceAsTheValueOfAFunctionIsReported() {
+        List<String> msgs = SourceValidator.validateYamlSimple(
+                """
+                        - from:
+                            uri: 
timer:orders?repeatCount=2&includeMetadata=true
+                            steps:
+                              - setBody:
+                                  expression:
+                                    simple:
+                                      expression: 
"${exchangeProperty.CamelTimerCounter == 1 ? 'resource:file:order.json' : 
'resource:file:order-bad-email.json'}"
+                              - setHeader:
+                                  name: fileUri
+                                  simple: 
"${exchangeProperty.CamelTimerCounter == 1 ? 'resource:file:order.json' : 'x'}"
+                              - setBody:
+                                  simple: "${header.file ?: 
'resource:file:order.json'}"
+                        """,
+                catalog);
+        assertThat(msgs).hasSize(3);
+        assertThat(msgs.get(0)).isEqualTo("Line 7: 
`${exchangeProperty.CamelTimerCounter == 1 ? 'resource:file:order.json' :"
+                                          + " 
'resource:file:order-bad-email.json'}` gives the text resource:file:order.json,"
+                                          + " not the file: a function's 
result is never loaded as a resource, only a"
+                                          + " resource: at the start of the 
expression is. Write resource:file: before"
+                                          + " the function: 
resource:file:${exchangeProperty.CamelTimerCounter == 1 ?"
+                                          + " 'order.json' : 
'order-bad-email.json'}");
+        assertThat(msgs.get(1)).startsWith("Line 10: 
`${exchangeProperty.CamelTimerCounter == 1 ? 'resource:file:order.json'"
+                                           + " : 'x'}` gives the text 
resource:file:order.json, not the file")
+                .endsWith("Write resource:file: at the start of the 
expression, and a function only for the part of the"
+                          + " name that changes, as in 
resource:file:order-${header.id}.json");
+        assertThat(msgs.get(2)).startsWith("Line 12: `${header.file ?: 
'resource:file:order.json'}` gives the text"
+                                           + " resource:file:order.json, not 
the file");
+        // the rewrite is accepted
+        assertThat(SourceValidator.validateYamlSimple(
+                """
+                        - from:
+                            uri: 
timer:orders?repeatCount=2&includeMetadata=true
+                            steps:
+                              - setBody:
+                                  simple: 
"resource:file:${exchangeProperty.CamelTimerCounter == 1 ? 'order.json' : 
'order-bad-email.json'}"
+                        """,
+                catalog)).isEmpty();
+    }
+
+    @Test
+    void aResourceThatIsLoadedOrNotAValueIsNotReported() {
+        List<String> msgs = SourceValidator.validateYamlSimple(
+                """
+                        - from:
+                            uri: 
timer:orders?repeatCount=2&includeMetadata=true
+                            steps:
+                              - setBody:
+                                  simple:
+                                    expression: 
"${exchangeProperty.CamelTimerCounter == 1 ? 'resource:file:order.json' : 
'resource:file:b.json'}"
+                                    resolveResource: true
+                              - setBody:
+                                  simple: 
"resource:file:order-${exchangeProperty.CamelTimerCounter}.json"
+                              - setBody:
+                                  simple: 
"${header.uri.replace('resource:file:', '')}"
+                              - filter:
+                                  simple: "${header.uri} == 
'resource:file:order.json'"
+                              - log:
+                                  message: "Loading ${header.uri} ?: 
'resource:file:order.json'"
+                              - setBody:
+                                  simple: "${body} ?: 
'resource:file:order.json'"
+                        """,
+                catalog);
+        assertThat(msgs).isEmpty();
+    }
+
     @Test
     void aResourceLocationWithoutATopLevelTernaryIsAccepted() {
         List<String> msgs = SourceValidator.validateYamlSimple(
@@ -772,6 +877,114 @@ class SourceValidatorSimpleTest {
                 .contains(".^[...] is 
SpEL").endsWith(PICK_BY_ID.substring(PICK_BY_ID.indexOf("        expression")));
     }
 
+    /**
+     * r27 rest-code-first step 1 (r27n-4): the filter written after the 
function. In a predicate the parser said only
+     * that operators and values must be separated by a space; in an 
expression it was text after the body.
+     */
+    @Test
+    void aFilterWrittenAfterTheFunctionIsReportedWithTheJsonPathStep() {
+        List<String> msgs = SourceValidator.validateYamlSimple("""
+                - from:
+                    uri: direct:get-order
+                    steps:
+                      - setHeader:
+                          name: foundOrder
+                          expression:
+                            simple:
+                              expression: "${body}?.?[orderId == 
'${header.id}']"
+                      - choice:
+                          when:
+                            - simple: "${header.id} != null && 
${body}?.?[orderId == '${header.id}'] != null"
+                              steps:
+                                - setBody:
+                                    simple: "${body}[?(@.orderId == 
'${header.id}')]"
+                """, catalog);
+        String pick = " To pick the element whose orderId is '${header.id}', 
set the body with jsonpath before this step;"
+                      + " the body is then that element, or null when none or 
several match (test that with ${body} =="
+                      + " null):\n" + PICK_BY_ID;
+        assertThat(msgs).hasSize(3);
+        assertThat(msgs.get(0)).isEqualTo("Line 8: Simple has no filter: 
.?[...] is SpEL, and after ${body} it is text, not"
+                                          + " a part of the function." + pick);
+        assertThat(msgs.get(1)).isEqualTo("Line 11: Simple has no filter: 
.?[...] is SpEL, and after ${body} it is text,"
+                                          + " not a part of the function." + 
pick);
+        assertThat(msgs.get(2)).isEqualTo("Line 14: Simple has no filter: 
[?(...)] is JsonPath, and after ${body} it is"
+                                          + " text, not a part of the 
function." + pick);
+        // from a header, as r27n-4 kept the list in one
+        assertThat(SimpleChecks.filterInFunction("${header.orders}?.?[orderId 
== '${header.id}']"))
+                .contains("after ${header.orders} it is text").endsWith("      
  source: header:orders\n"
+                                                                        + "    
    resultType: java.util.Map");
+        // text after a function that is no filter
+        for (String text : List.of("${header.name}.json", 
"order-${header.id}.csv", "${body}?.x", "${body} [?]")) {
+            assertThat(SimpleChecks.filterInFunction(text)).as(text).isNull();
+        }
+    }
+
+    /**
+     * r27 rest-code-first step 1 (r27n-7): ${size:${body}} passed the check 
and failed at runtime with "Unknown
+     * function: size:${body}", 4 errors before ${body.size()}. A nested 
function makes the parser read the outer one
+     * only when it runs.
+     */
+    @Test
+    void anUnknownFunctionWithANestedFunctionIsReported() {
+        List<String> msgs = SourceValidator.validateYamlSimple("""
+                - from:
+                    uri: direct:get-order
+                    steps:
+                      - choice:
+                          when:
+                            - simple: "${body} == null || ${size:${body}} == 0"
+                              steps:
+                                - setBody:
+                                    simple: "${foo:${header.id}}"
+                """, catalog);
+        assertThat(msgs).hasSize(2);
+        assertThat(msgs.get(0)).isEqualTo("Line 6: Simple has no function 
size:, so ${size:${body}} fails when the route"
+                                          + " runs (Unknown function: 
size:${body}): the number of elements of the body"
+                                          + " is ${body.size()}");
+        assertThat(msgs.get(1)).isEqualTo("Line 9: Simple has no function 
foo:, so ${foo:${header.id}} fails when the"
+                                          + " route runs (Unknown function: 
foo:${header.id})");
+        // functions that take a nested function
+        assertThat(SourceValidator.validateYamlSimple("""
+                - from:
+                    uri: direct:get-order
+                    steps:
+                      - setBody:
+                          simple: "${header.${header.name}} 
${variable.${header.key}} ${bean:${header.bean}?method=find}"
+                      - setBody:
+                          simple: "${date:header.${header.field}:yyyy} 
${properties:${header.key}} ${size(${body})}"
+                      - setBody:
+                          simple: "${body[${header.index}]} 
${exchangeProperty.${header.p}}"
+                """, catalog)).isEmpty();
+        // a custom function (a SimpleFunction bean, as in the simple-advanced 
documentation) is not in the catalog
+        assertThat(SourceValidator.validateYamlSimple("""
+                - from:
+                    uri: direct:contact
+                    steps:
+                      - setBody:
+                          simple: "Contact: 
${maskEmail(${header.customerEmail})}"
+                """, catalog)).isEmpty();
+    }
+
+    /**
+     * r27 rest-code-first step 2 (r27n-6): the method after the function, 
told only that a space is missing; the parser
+     * says now that it goes inside the braces, and the write check shows that.
+     */
+    @Test
+    void aMethodAfterTheFunctionSaysItGoesInside() {
+        List<String> msgs = SourceValidator.validateYamlSimple("""
+                - from:
+                    uri: direct:list-orders
+                    steps:
+                      - filter:
+                          simple: "${body.lines} != null && 
${body.lines}.size() > 0"
+                          steps:
+                            - log: "${body}"
+                """, catalog);
+        assertThat(msgs).singleElement().asString().startsWith("Line 5: Simple 
syntax error:")
+                .contains("the method .size() is outside the function, as text 
after its closing brace: write it inside"
+                          + " the braces, ${body.lines.size()}");
+    }
+
     @Test
     void whatIsNotAFilterOfAnotherLanguageIsNotReported() {
         // the jsonpath function of Simple, null-safe navigation, an index, 
and the syntax in a literal or outside ${...}
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
index 55637a4f9bc4..1a0aec4ed387 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
@@ -635,6 +635,11 @@ final class SchemaHints {
                                    + " steps: [...])"
                                  : "")
                          + "; several of them are several - " + m.name() + ": 
items"),
+            // otherwise: [- simple: ..., steps: [...], - otherwise: ...]: a 
second condition and its own otherwise
+            // written under otherwise:, whose steps: they are not (r27 
connect-http-client step 3: told that the write
+            // repairs the shape by itself, a model sent the same lines 16 
times)
+            append("type", ".*/otherwise", m -> m.message().contains("object 
expected") && heldCondition(m) != null,
+                    SchemaHints::conditionsUnderOtherwise),
             // otherwise: [- log: ...]: the block is a map whose steps: holds 
the list
             append("type", ".*/(otherwise|doTry|doFinally|doCatch/\\d+)", m -> 
m.message().contains("object expected"),
                     m -> {
@@ -699,6 +704,45 @@ final class SchemaHints {
             append("type", null, m -> m.message().contains("array expected"),
                     m -> "write it as a list: each item starts with \"- \""));
 
+    /**
+     * The key of the condition of the first item of an otherwise: list that 
is a condition with steps, as a when: item
+     * is (simple, expression...), or otherwise for an - otherwise: item; null 
when no item is one: the items are steps.
+     */
+    static String heldCondition(Match m) {
+        JsonNode instance = m.error().getInstanceNode();
+        if (instance == null || !instance.isArray()) {
+            return null;
+        }
+        for (JsonNode item : instance) {
+            if (!item.isObject()) {
+                continue;
+            }
+            if (item.has("otherwise") || item.has("when")) {
+                return "otherwise";
+            }
+            if (item.has("steps")) {
+                for (String key : (Iterable<String>) item::fieldNames) {
+                    if ("expression".equals(key) || 
m.validator().languageKeys().contains(key)) {
+                        return key;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /** What to write for an otherwise: list that holds a second condition, or 
an - otherwise: item of its own. */
+    static String conditionsUnderOtherwise(Match m) {
+        String key = heldCondition(m);
+        String condition = "otherwise".equals(key) ? "simple" : key;
+        String when = "expression".equals(condition)
+                ? "{expression: {simple: {expression: \"...\"}}, steps: [...]}"
+                : "{" + condition + ": \"...\", steps: [...]}";
+        return "otherwise holds only steps:, the steps for a message that no 
when matches. A second condition is another"
+               + " item of when:, with its " + condition + ": and its steps:, 
and the steps of an - otherwise: item go"
+               + " under otherwise: steps:, as in choice: {when: [" + when + 
", " + when + "], otherwise: {steps: [...]}}";
+    }
+
     // 
-------------------------------------------------------------------------------------------------------------
     // property hints
     // 
-------------------------------------------------------------------------------------------------------------
@@ -853,6 +897,11 @@ final class SchemaHints {
                         return "the endpoint of " + m.name() + " is an 
expression: write " + m.name()
                                + ": {expression: {constant: {expression: \"" + 
uri + "\"}}}";
                     }),
+            // when: [..., - otherwise: {steps: [...]}]: otherwise is a key of 
choice, not an item of its when: list (r27
+            // connect-http-client step 3)
+            unknownProperty(".*/when/\\d+", m -> 
m.unknown().equals("otherwise"),
+                    m -> "otherwise is not an item of when: it is a key of 
choice, next to when: and at its column, and"
+                         + " it holds only steps: choice: {when: [...], 
otherwise: {steps: [...]}}"),
             // choice: {steps: [...]}: the steps of a choice are under each 
when: and under otherwise: (r26
             // contracts-openapi-server step 4: told that choice owns a steps: 
list, a model wrote it 6 times)
             unknownProperty(".*/choice", m -> m.unknown().equals("steps"),
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
index 08736e81cdaf..d1ef342bdf44 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
@@ -841,6 +841,78 @@ public class YamlValidatorPropertyHintTest {
                 .contains("otherwise holds its EIPs under steps: otherwise: 
{steps: [- log: \"...\"]}"));
     }
 
+    /**
+     * r27l-4 connect-http-client step 3: a second condition and an - 
otherwise: item written under otherwise:. Told
+     * that otherwise holds its EIPs under steps:, a model sent the same lines 
16 times.
+     */
+    @Test
+    public void testOtherwiseHoldingASecondConditionSaysWhen() throws 
Exception {
+        List<Error> errors = validator.validate("""
+                - from:
+                    uri: timer:tick
+                    steps:
+                      - choice:
+                          when:
+                            - simple:
+                                expression: "${header.CamelHttpResponseCode} 
!= 200"
+                              steps:
+                                - log: "a"
+                          otherwise:
+                            - simple:
+                                expression: "${body[qty]} >= 
${variable.needed}"
+                              steps:
+                                - log: "b"
+                            - otherwise:
+                              steps:
+                                - log: "c"
+                """);
+        assertThat(errors).anySatisfy(e -> 
assertThat(e.getMessage()).contains("array found, object expected").endsWith(
+                "(otherwise holds only steps:, the steps for a message that no 
when matches."
+                                                                               
                                         + " A second condition is another item 
of when:, with its simple: and its steps:,"
+                                                                               
                                         + " and the steps of an - otherwise: 
item go under otherwise: steps:, as in choice:"
+                                                                               
                                         + " {when: [{simple: \"...\", steps: 
[...]}, {simple: \"...\", steps: [...]}],"
+                                                                               
                                         + " otherwise: {steps: [...]}})"));
+        // with expression: and the language under it, as the when items are 
written in the canonical form
+        errors = validator.validate("""
+                - from:
+                    uri: timer:tick
+                    steps:
+                      - choice:
+                          when:
+                            - expression:
+                                simple:
+                                  expression: "${body} != null"
+                              steps:
+                                - log: "a"
+                          otherwise:
+                            - expression:
+                                simple:
+                                  expression: "${body} == 'x'"
+                              steps:
+                                - log: "b"
+                """);
+        assertThat(errors).anySatisfy(e -> assertThat(e.getMessage())
+                .contains("with its expression: and its steps:")
+                .contains("{expression: {simple: {expression: \"...\"}}, 
steps: [...]}"));
+        // an - otherwise: item in the when: list
+        errors = validator.validate("""
+                - from:
+                    uri: timer:tick
+                    steps:
+                      - choice:
+                          when:
+                            - simple: "${body} != null"
+                              steps:
+                                - log: "a"
+                            - otherwise:
+                              steps:
+                                - log: "c"
+                """);
+        assertThat(errors).anySatisfy(e -> 
assertThat(e.getMessage()).contains("property 'otherwise' is not defined")
+                .contains("otherwise is not an item of when: it is a key of 
choice, next to when: and at its column, and"
+                          + " it holds only steps: choice: {when: [...], 
otherwise: {steps: [...]}}"));
+    }
+
     @Test
     public void testStepDirectlyUnderOtherwiseOrWhenSaysSteps() throws 
Exception {
         List<Error> errors = validator.validate("""

Reply via email to