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 9a77f79176fa CAMEL-26316: camel-jbang - AI tools: circuit breaker 
options in the route, a top-level ternary rewritten inside ${ }, and the cause 
of a sql error (#28498)
9a77f79176fa is described below

commit 9a77f79176facc403e86f95e8540f8bc733225e8
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 26 20:41:08 2026 +0100

    CAMEL-26316: camel-jbang - AI tools: circuit breaker options in the route, 
a top-level ternary rewritten inside ${ }, and the cause of a sql error (#28498)
    
    From the local-model benchmark of Oct 25-26 2026. camel_catalog_doc for 
resilience4j/faulttolerance points to the in-route circuit breaker 
configuration; a top-level ternary is shown rewritten inside one function; the 
validator reports :#{...} in sql URIs; a reload ERROR line ends with the root 
cause of its stack trace; camel_error_diagnose knows BadSqlGrammarException; 
the errorHandler example fails a message with a filter and throwException.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../modules/ROOT/pages/error-handler.adoc          | 22 +++++-
 .../dsl/jbang/core/commands/ai/eip-samples.json    |  2 +-
 .../dsl/jbang/core/commands/ai/CatalogDocs.java    | 29 ++++++-
 .../dsl/jbang/core/commands/ai/DiagnoseData.java   | 22 ++++++
 .../dsl/jbang/core/commands/ai/EndpointChecks.java | 27 +++++++
 .../dsl/jbang/core/commands/ai/ReloadOutcome.java  | 39 ++++++++++
 .../dsl/jbang/core/commands/ai/SimpleChecks.java   | 90 +++++++++++++++++++++-
 .../jbang/core/commands/ai/CatalogDocsTest.java    | 17 ++++
 .../jbang/core/commands/ai/ReloadOutcomeTest.java  | 35 +++++++++
 .../commands/ai/SourceValidatorSimpleTest.java     | 44 ++++++++++-
 .../ai/SourceValidatorSqlParametersTest.java       | 37 +++++++++
 11 files changed, 352 insertions(+), 12 deletions(-)

diff --git a/docs/user-manual/modules/ROOT/pages/error-handler.adoc 
b/docs/user-manual/modules/ROOT/pages/error-handler.adoc
index b5af1a4d6beb..3fbf8ef01979 100644
--- a/docs/user-manual/modules/ROOT/pages/error-handler.adoc
+++ b/docs/user-manual/modules/ROOT/pages/error-handler.adoc
@@ -249,6 +249,10 @@ In this example, we configure a 
xref:components:eips:dead-letter-channel.adoc[De
 Channel] on the route that should redeliver at most 3 times and use a
 little delay before retrying.
 
+The route fails on purpose when an order is not reserved: a 
xref:components:eips:filter-eip.adoc[Filter]
+with xref:components:eips:throwException-eip.adoc[Throw Exception] in it. 
After the redeliveries,
+the order goes to the dead letter endpoint. Any exception does the same, such 
as one that a bean or a processor throws.
+
 [tabs]
 ====
 
@@ -263,7 +267,10 @@ XML::
         </deadLetterChannel>
     </errorHandler>
     <from uri="direct:in"/>
-    <process ref="myFailureProcessor"/>
+    <filter>
+        <simple>${header.status} != 'reserved'</simple>
+        <throwException exceptionType="java.lang.IllegalStateException" 
message="Order ${header.orderId} is not reserved"/>
+    </filter>
     <to uri="mock:result"/>
 </route>
 ----
@@ -302,7 +309,10 @@ etc.
         <!-- set the errorHandlerRef to our DeadLetterChannel, this applies 
for this route only -->
         <route errorHandlerRef="myDeadLetterErrorHandler">
             <from uri="direct:in"/>
-            <process ref="myFailureProcessor"/>
+            <filter>
+                <simple>${header.status} != 'reserved'</simple>
+                <throwException 
exceptionType="java.lang.IllegalStateException" message="Order 
${header.orderId} is not reserved"/>
+            </filter>
             <to uri="mock:result"/>
         </route>
     </camelContext>
@@ -322,8 +332,12 @@ YAML::
     from:
       uri: direct:in
       steps:
-        - process:
-            ref: myFailureProcessor
+        - filter:
+            simple: "${header.status} != 'reserved'"
+            steps:
+              - throwException:
+                  exceptionType: java.lang.IllegalStateException
+                  message: "Order ${header.orderId} is not reserved"
         - to:
             uri: mock:result
 ----
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
index f4aa3c78cb1a..42e193f5a7b0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/org/apache/camel/dsl/jbang/core/commands/ai/eip-samples.json
@@ -384,7 +384,7 @@
   "errorHandler": [
     {
       "source": "error-handler.adoc",
-      "yaml": "- route:\n    errorHandler:\n      deadLetterChannel:\n        
deadLetterUri: mock:dead\n        redeliveryPolicy:\n          
maximumRedeliveries: 3\n          redeliveryDelay: 250\n    from:\n      uri: 
direct:in\n      steps:\n        - process:\n            ref: 
myFailureProcessor\n        - to:\n            uri: mock:result\n"
+      "yaml": "- route:\n    errorHandler:\n      deadLetterChannel:\n        
deadLetterUri: mock:dead\n        redeliveryPolicy:\n          
maximumRedeliveries: 3\n          redeliveryDelay: 250\n    from:\n      uri: 
direct:in\n      steps:\n        - filter:\n            simple: 
\"${header.status} != 'reserved'\"\n            steps:\n              - 
throwException:\n                  exceptionType: 
java.lang.IllegalStateException\n                  message: \"Order 
${header.orderId} i [...]
     },
     {
       "source": "error-handler.adoc",
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
index 9fee53d39d85..bbbd27bb3932 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
@@ -914,10 +914,19 @@ public final class CatalogDocs {
         return type.equals(element.getJavaType());
     }
 
+    /**
+     * The main option groups of a circuit breaker implementation, with the 
option of the circuitBreaker EIP that
+     * configures one circuit breaker in the route.
+     */
+    static final Map<String, String> CIRCUIT_BREAKER_CONFIGURATIONS = Map.of(
+            "camel.resilience4j", "resilience4jConfiguration",
+            "camel.faulttolerance", "faultToleranceConfiguration");
+
     /**
      * The camel.&lt;name&gt;.* main options when the name is one of their 
groups (resilience4j, faulttolerance, health,
      * metrics, threadpool, rest, ...), else null. A model asks for 
"resilience4j" as a component when it wants the
-     * application.properties keys of the circuit breaker.
+     * application.properties keys of the circuit breaker, or when it wants to 
configure one circuit breaker, which is
+     * done in the route: the answer says so first.
      */
     static JsonObject mainOptionsGroup(CamelCatalog catalog, String name) {
         String n = name.trim().toLowerCase(Locale.ROOT);
@@ -978,11 +987,25 @@ public final class CatalogDocs {
             result.put("note", "the " + group + ".* keys go in 
application.properties and are read by camel run and"
                                + " camel export, not by the Camel application 
itself");
         } else if (matched != null) {
+            // a reload applies the defaults of the circuit breakers 
(CAMEL-26320), not the other main options
             result.put("note", matched.getName() + " is a key for 
application.properties (camel run and Camel Main read"
-                               + " it at startup); the other " + group + ".* 
keys follow it");
+                               + " it at startup" + 
(CIRCUIT_BREAKER_CONFIGURATIONS.containsKey(group)
+                                       ? "" : ", so a reload of the running 
integration does not apply it")
+                               + "); the other " + group + ".* keys follow 
it");
+        } else if (CIRCUIT_BREAKER_CONFIGURATIONS.containsKey(group)) {
+            // asked for "resilience4j" as a component to "add a 
resilience4jConfiguration", a model wrote these keys to
+            // application.properties instead of configuring the circuit 
breaker of its route (4 of 7 runs of a step in
+            // the local-model benchmark)
+            String eipOption = CIRCUIT_BREAKER_CONFIGURATIONS.get(group);
+            result.put("note", name + " is not a component: it runs the 
circuitBreaker EIP. To configure a circuit breaker,"
+                               + " set its options in the route, under 
circuitBreaker: " + eipOption + ": (camel_catalog_doc"
+                               + " name=" + eipOption + " kind=eip has them). 
The " + group + ".* keys below are the"
+                               + " defaults of every circuit breaker, for 
application.properties");
+            result.put("inTheRoute", "circuitBreaker: {" + eipOption + ": 
{...}}");
         } else {
             result.put("note", name + " is not a component: these are the " + 
group + ".* keys for"
-                               + " application.properties (camel run reads 
them at startup)");
+                               + " application.properties (camel run reads 
them at startup, so a reload of the running"
+                               + " integration does not apply them)");
         }
         if (matched != null) {
             result.put("matchedKey", matched.getName());
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DiagnoseData.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DiagnoseData.java
index c2189f079303..7d718b94c253 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DiagnoseData.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/DiagnoseData.java
@@ -390,6 +390,28 @@ public class DiagnoseData {
                 List.of(
                         CAMEL_MANUAL_DOC + "security.html")));
 
+        // the sql component runs its statements with Spring JDBC: "bad SQL 
grammar []" alone was diagnosed as no known
+        // exception, and the database's own message was only in the stack 
trace (the local-model benchmark)
+        exceptions.put("BadSqlGrammarException", new ExceptionInfo(
+                "The database refused the SQL statement (Spring JDBC, which 
the sql component uses). The database's own"
+                                                                   + " message 
is in the cause: the Caused by line"
+                                                                   + " of the 
stack trace (camel_get_log) names the"
+                                                                   + " error 
and its position in the statement.",
+                List.of(
+                        "A parameter in a form the sql component does not 
read, such as :#{body.customer}: it reads"
+                        + " :#name (a key of the body when the body is a Map, 
or a header) and :#${...} (a simple"
+                        + " expression), and sends anything else to the 
database as written",
+                        "An insert-or-update in the dialect of another 
database (ON DUPLICATE KEY UPDATE is MySQL,"
+                                                                               
               + " ON CONFLICT ... DO UPDATE is PostgreSQL)",
+                        "A table or column that does not exist, or a typo in a 
keyword",
+                        "A parameter where SQL allows none, such as a table or 
column name"),
+                List.of(
+                        "Read the Caused by line in camel_get_log: it has the 
database's message and the position",
+                        "Write parameters as :#${body[customer]} or 
:#customer",
+                        "camel_runtime_sql_trace shows the statements as the 
application sent them"),
+                List.of(
+                        CAMEL_COMPONENT_DOC + "sql-component.html")));
+
         KNOWN_EXCEPTIONS = Collections.unmodifiableMap(exceptions);
     }
 
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 2d5410a8a8ee..6924f9fe6cbe 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
@@ -492,6 +492,14 @@ final class EndpointChecks {
     /** :#excluded.country, :#EXCLUDED.country: the row being inserted written 
as a named parameter. */
     private static final Pattern EXCLUDED_PARAMETER = 
Pattern.compile("(?i):#(excluded)\\.(\\w+)");
 
+    /**
+     * :#{body.customer}, the expression form of other frameworks: camel-sql 
reads :#name and :#${simple}, so the JDBC
+     * driver gets it as written. Three runs of a step in the local-model 
benchmark wrote it, the answer said only "bad
+     * SQL grammar []", and they fixed it after 69 to 92 failed messages.
+     */
+    private static final Pattern BRACE_PARAMETER = 
Pattern.compile(":#\\{([^}]*)}");
+    private static final Pattern BODY_FIELD = 
Pattern.compile("^body\\.(\\w+)$");
+
     static void checkSqlNamedParameters(
             List<String> errors, String fullUri, String rawLine, int 
uriLineIdx, Map<String, Integer> optionLineMap) {
         int colon = fullUri.indexOf(':');
@@ -549,6 +557,25 @@ final class EndpointChecks {
                        + " being inserted in ON CONFLICT ... DO UPDATE, write 
EXCLUDED." + first + " without :#"
                        + (others.isEmpty() ? "" : " (also " + String.join(", 
", others) + ")"));
         }
+        Matcher brace = BRACE_PARAMETER.matcher(query);
+        List<String> braced = new ArrayList<>();
+        while (brace.find()) {
+            if (!braced.contains(brace.group())) {
+                braced.add(brace.group());
+            }
+        }
+        if (!braced.isEmpty()) {
+            Matcher inner = BRACE_PARAMETER.matcher(braced.get(0));
+            String expression = inner.matches() ? inner.group(1).strip() : "";
+            Matcher field = BODY_FIELD.matcher(expression);
+            String fix = field.matches()
+                    ? ":#${body[" + field.group(1) + "]} (a Simple expression) 
or :#" + field.group(1)
+                      + " for a header or a key of a Map body"
+                    : ":#${" + expression + "} (a Simple expression) or :#name 
for a header or a key of a Map body";
+            errors.add(linePrefix(line) + "sql: " + braced.get(0) + " is not a 
camel-sql named parameter (the JDBC driver"
+                       + " gets it as written and fails with a syntax error): 
write " + fix
+                       + (braced.size() > 1 ? " (also " + String.join(", ", 
braced.subList(1, braced.size())) + ")" : ""));
+        }
         Matcher m = BARE_NAMED_PARAMETER.matcher(query);
         List<String> bare = new ArrayList<>();
         while (m.find()) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
index 13df72e1dfcb..89b048014e46 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcome.java
@@ -252,6 +252,42 @@ public final class ReloadOutcome {
         return sb.toString();
     }
 
+    private static final String CAUSED_BY = "Caused by: ";
+    private static final int MAX_CAUSE = 300;
+
+    /**
+     * The line with the root cause of its stack trace when the line does not 
say it already: the last "Caused by" and
+     * the lines that continue its message (PostgreSQL gives the position of 
the error on the next line). A sql: route
+     * failed with "BadSqlGrammarException: PreparedStatementCallback; bad SQL 
grammar []", and the database's message
+     * ("ERROR: syntax error at or near ":"") was only in the stack trace (the 
local-model benchmark).
+     */
+    static String withRootCause(String line, String detail) {
+        int at = detail != null ? detail.lastIndexOf(CAUSED_BY) : -1;
+        if (at < 0) {
+            return line;
+        }
+        String[] lines = detail.substring(at + CAUSED_BY.length()).split("\n");
+        StringBuilder cause = new StringBuilder(lines[0].strip());
+        for (int i = 1; i < lines.length && i < 3; i++) {
+            String next = lines[i].strip();
+            if (next.isEmpty() || next.startsWith("at ") || 
next.startsWith("... ")) {
+                break;
+            }
+            cause.append(' ').append(next);
+        }
+        String text = compact(cause.toString());
+        // the message of the cause, without its class: the line may have it 
already (a "due to" of Camel)
+        int colon = text.indexOf(": ");
+        String message = colon > 0 ? text.substring(colon + 2) : text;
+        if (message.isBlank() || line.contains(message)) {
+            return line;
+        }
+        if (text.length() > MAX_CAUSE) {
+            text = text.substring(0, MAX_CAUSE) + "...";
+        }
+        return line + " (caused by " + text + ")";
+    }
+
     static JsonArray output(List<JsonObject> newestFirst) {
         List<String> lines = new ArrayList<>();
         for (JsonObject r : newestFirst) {
@@ -274,6 +310,9 @@ public final class ReloadOutcome {
             int max = OUTPUT_CHARS;
             if (!routeLog) {
                 m = compact(m);
+                if ("ERROR".equalsIgnoreCase(level)) {
+                    m = withRootCause(m, detail);
+                }
                 max = OUTPUT_ERROR_CHARS;
             }
             lines.add((routeLog ? "" : level.toUpperCase(Locale.ROOT) + " ")
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 d312ac576205..e5ee4d6e3bdd 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,8 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.LanguageValidationResult;
@@ -184,12 +186,96 @@ final class SimpleChecks {
             return null;
         }
         int question = indexOfTopLevel(text, " ? ", 0);
-        if (question < 0 || indexOfTopLevel(text, " : ", question + 3) < 0) {
+        int colon = question < 0 ? -1 : indexOfTopLevel(text, " : ", question 
+ 3);
+        if (colon < 0) {
             return null;
         }
+        // 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);
         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, as in 
${body.size() == 0 ? ${null} : ${body[0]}}";
+               + (rewritten != null
+                       ? " Write the whole ternary inside one function: " + 
rewritten
+                       : " Write the whole ternary inside one function, as in 
${body.size() == 0 ? ${null} : ${body[0]}}");
+    }
+
+    /** A binary operator of Simple at the end of the text, after a function: 
the function is part of the condition. */
+    private static final Pattern OPERATOR_AFTER_A_FUNCTION = Pattern.compile(
+            
"}\\s*(?:==|!=|>=|<=|>|<|=~|!=~|contains|!contains|~~|regex|!regex|in|!in|range|!range|startsWith|endsWith"
+                                                                             + 
"|is|!is)\\s*$");
+
+    /**
+     * The top-level ternary written inside one function, or null when no 
function comes before the <tt>?</tt>: the
+     * function the condition starts with is opened before the condition and 
closed after the second value, and a value
+     * written as bare text is quoted. 
<tt>resource:file:${exchangeProperty.CamelTimerCounter} == 1 ? order.json :
+     * order-bad-email.json</tt> is 
<tt>resource:file:${exchangeProperty.CamelTimerCounter == 1 ? 'order.json' :
+     * 'order-bad-email.json'}</tt>.
+     */
+    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
+        List<int[]> functions = new ArrayList<>();
+        for (int i = 0; i < question; i++) {
+            if (text.charAt(i) == '$' && i < text.length() - 1 && 
text.charAt(i + 1) == '{') {
+                int close = closingBrace(text, i);
+                if (close < 0 || close > question) {
+                    return null;
+                }
+                functions.add(new int[] { i, close });
+                i = close;
+            }
+        }
+        if (functions.isEmpty()) {
+            return null;
+        }
+        // the last function before the ?, or for ${a} == ${b} ? x : y the 
function before the operator
+        int index = functions.size() - 1;
+        int[] last = functions.get(index);
+        Matcher op = OPERATOR_AFTER_A_FUNCTION.matcher(text.substring(0, 
last[0]));
+        if (index > 0 && op.find() && op.start() == functions.get(index - 
1)[1]) {
+            index--;
+        }
+        int start = functions.get(index)[0];
+        int end = functions.get(index)[1];
+        String condition = text.substring(start + 2, end) + text.substring(end 
+ 1, question);
+        return text.substring(0, start) + "${" + condition + " ? " + 
quoted(text.substring(question + 3, colon).trim())
+               + " : " + quoted(text.substring(colon + 3).trim()) + "}";
+    }
+
+    /** 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;
+        int depth = 0;
+        for (int i = open; i < text.length(); i++) {
+            char ch = text.charAt(i);
+            if (quote != 0) {
+                if (ch == quote) {
+                    quote = 0;
+                }
+            } else if (ch == '\'' || ch == '"') {
+                quote = ch;
+            } else if (ch == '$' && i < text.length() - 1 && text.charAt(i + 
1) == '{') {
+                depth++;
+                i++;
+            } else if (ch == '{') {
+                // a brace of the function's own text, such as a closure 
${body.find { it.x }}
+                depth++;
+            } else if (ch == '}' && --depth == 0) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * A value of the ternary: bare text is quoted, a quoted literal, a 
function, a number, true, false and null not.
+     */
+    private static String quoted(String value) {
+        if (value.isEmpty() || value.startsWith("'") || value.startsWith("\"") 
|| value.startsWith("${")
+                || value.matches("-?\\d+(\\.\\d+)?|true|false|null") || 
value.contains("'")) {
+            return value;
+        }
+        return "'" + value + "'";
     }
 
     /** The index of the token at the top level of the text: outside every 
${...} and outside every quoted literal. */
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
index 2dcacc3ef74b..ab7d10e13e1a 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
@@ -543,6 +543,23 @@ class CatalogDocsTest {
         assertEquals("camel.resilience4j", o.getString("group"));
         assertTrue(o.getString("note").contains("not a component"), 
o.getString("note"));
         assertTrue(json.contains("camel.resilience4j.failureRateThreshold"), 
json);
+        // the route first: asked this way to "add a 
resilience4jConfiguration", a model wrote the keys to
+        // application.properties instead of configuring the circuit breaker 
of its route (the local-model benchmark)
+        String note = o.getString("note");
+        assertTrue(note.contains("set its options in the route, under 
circuitBreaker: resilience4jConfiguration:"), note);
+        assertTrue(note.indexOf("circuitBreaker:") < 
note.indexOf("camel.resilience4j.*"), note);
+        // a reload applies the defaults of the circuit breakers (CAMEL-26320)
+        assertFalse(note.contains("does not apply"), note);
+        assertFalse(catalogDoc(Map.of("name", 
"camel.resilience4j.slidingWindowSize")).getString("note")
+                .contains("does not apply"));
+        assertEquals("circuitBreaker: {resilience4jConfiguration: {...}}", 
o.getString("inTheRoute"));
+        JsonObject faultTolerance = catalogDoc(Map.of("name", 
"faulttolerance", "kind", "component"));
+        assertEquals("circuitBreaker: {faultToleranceConfiguration: {...}}", 
faultTolerance.getString("inTheRoute"));
+        // a group without a route option says only where its keys go, and 
that a reload does not apply them
+        JsonObject threadPool = catalogDoc(Map.of("name", "threadpool", 
"kind", "component"));
+        assertEquals("camel.threadpool", threadPool.getString("group"));
+        assertNull(threadPool.get("inTheRoute"));
+        assertTrue(threadPool.getString("note").contains("does not apply 
them"), threadPool.getString("note"));
 
         // no kind: the same answer
         args.remove("kind");
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
index befcec4ff7af..44e1d948a569 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ReloadOutcomeTest.java
@@ -188,6 +188,41 @@ class ReloadOutcomeTest {
         assertTrue(!line.contains("MessageId") && !line.contains("Exhausted 
after"), line);
     }
 
+    /**
+     * A sql: route failed with "bad SQL grammar []", and the database's 
message was only in the stack trace (the
+     * local-model benchmark): the line ends with the root cause, with the 
position PostgreSQL gives on its next line.
+     */
+    @Test
+    void theRootCauseOfAFailedExchangeIsAddedWhenTheLineDoesNotSayIt() {
+        List<String> failed = List.of(
+                "2026-10-25 22:20:53.000  INFO 42 --- [rReloadStrategy] 
org.apache.camel.support.RouteWatcherReloadStrategy : Routes reloaded summary 
(total:2 started:2 changed:1)",
+                "2026-10-25 22:20:54.029 ERROR 42 --- [- file://orders] 
or.errorhandler.DefaultErrorHandler : Failed delivery for"
+                                                                               
                                                                                
                       + " (MessageId: D53CDBC0CA79126-02 on ExchangeId: 
D53CDBC0CA79126-02) at"
+                                                                               
                                                                                
                       + " register-customers[to3] sql.camel.yaml:29. Exhausted 
after delivery attempt: 1"
+                                                                               
                                                                                
                       + " caught: 
org.springframework.jdbc.BadSqlGrammarException:"
+                                                                               
                                                                                
                       + " PreparedStatementCallback; bad SQL grammar []",
+                "",
+                "org.springframework.jdbc.BadSqlGrammarException: 
PreparedStatementCallback; bad SQL grammar []",
+                "\tat 
org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:112)",
+                "Caused by: org.postgresql.util.PSQLException: ERROR: syntax 
error at or near \":\"",
+                "  Position: 53",
+                "\tat 
org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2993)");
+
+        JsonArray output = ReloadOutcome.output(records(BEFORE, failed));
+
+        assertEquals(1, output.size(), output.toJson());
+        assertEquals("ERROR Failed at register-customers[to3] 
sql.camel.yaml:29: BadSqlGrammarException:"
+                     + " PreparedStatementCallback; bad SQL grammar [] (caused 
by PSQLException: ERROR: syntax error at or"
+                     + " near \":\" Position: 53)",
+                output.get(0).toString());
+
+        // a cause the line already says is not repeated
+        assertEquals("Failed: X due to: IllegalStateException: no stock",
+                ReloadOutcome.withRootCause("Failed: X due to: 
IllegalStateException: no stock",
+                        "Caused by: java.lang.IllegalStateException: no 
stock\n\tat a.B.c(B.java:1)"));
+        assertEquals("Failed: X", ReloadOutcome.withRootCause("Failed: X", "no 
stack trace"));
+    }
+
     @Test
     void theDumpOfAnExchangeAndOfALongBeanGo() {
         String m = ReloadOutcome.compact("Method with name: orderId not found 
on bean: ["
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 a2365b99a20a..74a7261735b7 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
@@ -431,7 +431,46 @@ class SourceValidatorSimpleTest {
         assertThat(msgs.get(0)).startsWith("Line 5:")
                 .contains("Simple has no top-level ternary")
                 .contains("the ? at index 20")
-                .contains("inside one function");
+                .endsWith("Write the whole ternary inside one function: 
${body.size() == 0 ? ${null} : ${body[0]}}");
+    }
+
+    /**
+     * The message shows 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).
+     */
+    @Test
+    void aTopLevelTernaryIsShownInsideOneFunction() {
+        assertThat(rewritten("${header.CamelTimerCounter} == 1 ? 'order.json' 
: 'order-bad-email.json'"))
+                .isEqualTo("${header.CamelTimerCounter == 1 ? 'order.json' : 
'order-bad-email.json'}");
+        // the literal text before the condition stays, and a bare value is 
quoted
+        
assertThat(rewritten("resource:file:${exchangeProperty.CamelTimerCounter} == 1 
? order.json : order-bad-email.json"))
+                .isEqualTo("resource:file:${exchangeProperty.CamelTimerCounter 
== 1 ? 'order.json' : 'order-bad-email.json'}");
+        assertThat(rewritten("Order ${header.id}: ${header.qty} > 5 ? big : 
small"))
+                .isEqualTo("Order ${header.id}: ${header.qty > 5 ? 'big' : 
'small'}");
+        // a brace of the function's own text does not end it
+        assertThat(rewritten("${body.find { it.x == 1 }} != null ? found : 
none"))
+                .isEqualTo("${body.find { it.x == 1 } != null ? 'found' : 
'none'}");
+        // two functions compared: the condition starts with the first of them
+        assertThat(rewritten("${header.a} == ${header.b} ? same : 2"))
+                .isEqualTo("${header.a == ${header.b} ? 'same' : 2}");
+        // no function before the ?: no rewrite, the message keeps its example
+        assertThat(SimpleChecks.topLevelTernary("Total 5 > 3 ? ${header.a} : 
no"))
+                .endsWith("as in ${body.size() == 0 ? ${null} : ${body[0]}}");
+        // the rewritten expression is accepted
+        assertThat(SourceValidator.validateYamlSimple("""
+                - from:
+                    uri: timer:tick
+                    steps:
+                      - setBody:
+                          simple: "%s"
+                """.formatted(rewritten("${header.CamelTimerCounter} == 1 ? 
'order.json' : 'order-bad-email.json'")),
+                catalog)).isEmpty();
+    }
+
+    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());
     }
 
     @Test
@@ -446,7 +485,8 @@ class SourceValidatorSimpleTest {
                           simple: 
"resource:file:${exchangeProperty.CamelTimerCounter} == 1 ? order.json : 
order-bad-email.json"
                 """, catalog);
         assertThat(msgs).hasSize(1);
-        assertThat(msgs.get(0)).startsWith("Line 5:").contains("Simple has no 
top-level ternary");
+        assertThat(msgs.get(0)).startsWith("Line 5:").contains("Simple has no 
top-level ternary")
+                .endsWith("resource:file:${exchangeProperty.CamelTimerCounter 
== 1 ? 'order.json' : 'order-bad-email.json'}");
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSqlParametersTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSqlParametersTest.java
index 6aac64e0b9e4..85cb897b7fe4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSqlParametersTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorSqlParametersTest.java
@@ -65,6 +65,43 @@ class SourceValidatorSqlParametersTest {
                 && e.contains("write :#${body[customer]} (a Simple 
expression)"));
     }
 
+    /**
+     * :#{body.customer} goes to the JDBC driver as written: three runs of a 
step in the local-model benchmark wrote it,
+     * and the runtime said only "bad SQL grammar []".
+     */
+    @Test
+    void theBraceFormOfOtherFrameworksGetsTheSimpleForm() {
+        String yaml
+                = """
+                        - route:
+                            from:
+                              uri: file:orders
+                              steps:
+                                - to:
+                                    uri: "sql:INSERT INTO customers (id, 
country, orders) VALUES (:#{body.customer}, :#{body.country}, 1)"
+                        """;
+        List<String> errors = SourceValidator.validateYamlEndpoints(yaml, 
catalog);
+        assertThat(errors).anyMatch(e -> e.startsWith("Line 6: sql: 
:#{body.customer} is not a camel-sql named parameter")
+                && e.contains(
+                        "write :#${body[customer]} (a Simple expression) or 
:#customer for a header or a key of a Map body")
+                && e.endsWith("(also :#{body.country})"));
+
+        // under parameters, and an expression that is not a field of the body
+        yaml = """
+                - route:
+                    from:
+                      uri: file:orders
+                      steps:
+                        - to:
+                            uri: sql
+                            parameters:
+                              query: "SELECT * FROM customers WHERE id = 
:#{header.id}"
+                """;
+        errors = SourceValidator.validateYamlEndpoints(yaml, catalog);
+        assertThat(errors).anyMatch(e -> e.startsWith("Line 8: sql: 
:#{header.id} is not a camel-sql named parameter")
+                && e.contains("write :#${header.id} (a Simple expression)"));
+    }
+
     @Test
     void theCamelFormsCastsTimesAndOtherComponentsAreFine() {
         String yaml

Reply via email to