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 b5267ea19501 CAMEL-26960: camel-jbang - mark the mistakes in the code 
of AI answers, such as options that do not exist (#29906)
b5267ea19501 is described below

commit b5267ea19501ed4a61665de935e48f97d98d99f6
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 14 20:24:34 2026 +0100

    CAMEL-26960: camel-jbang - mark the mistakes in the code of AI answers, 
such as options that do not exist (#29906)
    
    The code in an AI answer is what a user copies, but only its simple 
expressions were checked (CAMEL-24805). The AI
    panel and camel ask now check the code blocks of every answer against the 
catalog, and mark what does not exist,
    with no correction turn:
    - YAML: unknown keys and options from the YAML DSL schema, with fragments 
(a from: without its route, steps, one
      EIP, redeliveryPolicy:, resilience4jConfiguration:) wrapped in what they 
belong to (YamlFragment).
    - Endpoint uris: options the component does not have.
    - camel.* keys of properties lines, in a block of any language.
    - A value of the wrong type, only when the validator has a hint for it.
    What a snippet leaves out on purpose, keys where the wrapping put a 
fragment, and parse errors are not marked. The
    AI panel shows a line under the block; camel ask prints a Check: line on 
stderr. The model is told of the marks
    with the next question, and its own answer stays in the history as it wrote 
it.
    
    camel validate properties and the AI checks report a camel.* key of a group 
Camel does not have, which camel-main
    drops without a word, when a Camel key is near it (camel.tracing.enabled -> 
camel.trace.enabled), and
    camel.logging.level with logging.level.root. A jdbc: endpoint written as a 
JDBC url is told that jdbc: takes the
    name of a DataSource bean. camel_properties_translate takes camel.trace., 
not camel.tracing., as a Camel prefix.
    
    Two validator bugs: a commented-out option under parameters: was an unknown 
option, and a bean with its name: and
    an unknown key was told the name is the key.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   9 +
 .../modules/ROOT/pages/camel-jbang-ai.adoc         |  12 +
 .../modules/ROOT/pages/camel-jbang-tui-ai.adoc     |  19 ++
 .../modules/ROOT/pages/camel-jbang-validation.adoc |   7 +
 .../apache/camel/dsl/jbang/core/commands/Ask.java  |  12 +-
 .../dsl/jbang/core/commands/ai/AnswerChecks.java   | 281 +++++++++++++++++++++
 .../dsl/jbang/core/commands/ai/EndpointChecks.java |  11 +-
 .../jbang/core/commands/ai/PropertiesChecks.java   | 119 ++++++++-
 .../dsl/jbang/core/commands/ai/YamlFragment.java   | 145 +++++++++++
 .../camel/dsl/jbang/core/commands/AskLoopTest.java |  14 +
 .../core/commands/ai/AnswerCodeChecksTest.java     | 265 +++++++++++++++++++
 .../commands/ai/SourceValidatorEndpointTest.java   |  33 +++
 .../commands/ai/SourceValidatorPropertiesTest.java |  58 +++++
 .../commands/mcp/PropertiesTranslateTools.java     |   2 +-
 .../commands/mcp/PropertiesTranslateToolsTest.java |  10 +
 .../camel/dsl/jbang/core/commands/tui/AiPanel.java |  10 +-
 .../dsl/jbang/core/commands/tui/AiPanelTest.java   |  63 +++++
 .../camel/dsl/yaml/validator/SchemaHints.java      |  16 +-
 .../validator/YamlValidatorPropertyHintTest.java   |  25 ++
 19 files changed, 1101 insertions(+), 10 deletions(-)

diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 92eaa8b11639..ad8cd3d20a85 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -2198,6 +2198,15 @@ that a route sends to and no route consumes, so they 
report the same endpoints o
 
 So `camel validate` can report an endpoint it did not report before.
 
+=== camel-jbang - camel validate properties reports a key of a group Camel 
does not have
+
+`camel validate properties`, `camel validate source` and the AI file tools now 
report a `camel.*` key of a group
+Camel does not have when a Camel key is near it, such as 
`camel.tracing.enabled` (for `camel.trace.enabled`),
+`camel.profile` (for `camel.main.profile`) and `camel.kafka.brokers` (for 
`camel.component.kafka.brokers`), and
+`camel.logging.level` (for `logging.level.root`). Camel
+ignores such a key without a warning, so the line had no effect. A file with 
one, which passed before, now fails the
+check.
+
 === camel-jbang - the AI file tools stay inside the project directory, also 
through a symbolic link
 
 The file tools of `camel mcp`, `camel ask` and the TUI AI agents 
(`camel_validate_source`, `camel_get_files`,
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc
index 8cfb5d7eab93..b86891d11c63 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-ai.adoc
@@ -59,6 +59,18 @@ With a question, the answer is the only output on stdout, so 
a script can use it
 `--show-tools` and the token usage of `--show-stats` are printed on stderr. 
`camel ask` exits with `1` when the LLM
 cannot answer, such as when it is not reachable or the call fails.
 
+The code in an answer is checked against the Camel catalog, because it is what 
you copy. An invalid simple expression gets
+the model one turn to fix it. What does not exist in the code blocks is 
printed on stderr under the answer, one
+`Check:` line each: a key or an option the YAML DSL does not have, an option 
the component of an endpoint does not
+have, and a `camel.*` key that is not a Camel option. For example:
+
+[source,text]
+----
+Check: code block 1, line 4: `deleteAfterRead` is not an option of the `file` 
component (did you mean `delete`?)
+----
+
+In a chat, the model is told of them with your next question, so its next 
answer does not repeat them.
+
 === What asks first
 
 Most tools only read, and run without asking. A call that changes the project,
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc
index a0108b4ad1c1..3589fc185d11 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-ai.adoc
@@ -183,6 +183,25 @@ does not fix it the answer is shown as it is with a 
*Simple check* line under it
 and the error, and the AI log has the exchange. Placeholders that are not 
simple (a Maven
 `$\{camel-version}`, a shell variable) are left alone.
 
+The code blocks of an answer are checked for what does not exist: a key or an 
option the YAML DSL does
+not have (`multiplier` in a `redeliveryPolicy`, where the option is 
`backOffMultiplier`), an option the
+component of an endpoint does not have (`deleteAfterRead` on `file`), and a 
`camel.*` key that is not a
+Camel option (`camel.server.context-path`), in any block. A snippet is checked 
as a part of what it
+belongs to: a `from:` without its route, a list of steps and a single EIP as 
the steps of a route, and a
+`redeliveryPolicy:` under an `onException`. Each mistake is marked with a line 
under its code block,
+with its line in the block and the hint of the validator when it has one:
+
+[source,text]
+----
+āœ— line 5: `deleteAfterRead` is not an option of the `file` component (did you 
mean `delete`?)
+----
+
+The answer is not sent back for a correction, which would cost a local model 
the whole answer again.
+Your next question tells the model about the marks and any simple expression 
it did not fix, so
+its next answer does not repeat them. What a snippet leaves out on purpose is 
not marked: a required
+option, the steps of a route, a placeholder value, the options of an EIP shown 
without the EIP. *Copy*
+still copies only the code of a block.
+
 Press *Ctrl+U* while the AI panel is open to toggle the AI Usage view. It 
shows token consumption from
 the embedded AI prompt (*TUI ask*) and from LLM calls made by the monitored 
integration (*integration*),
 the latter taken from OpenTelemetry GenAI spans when observability is enabled 
on the integration. The
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
index 709da62cf506..b858935f1903 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-validation.adoc
@@ -124,6 +124,13 @@ is reported before the application fails to start:
 camel validate properties application.properties application-prod.properties
 ----
 
+A key of a group Camel does not have is reported too, as Camel ignores it 
without a warning: the line has no effect.
+The message names the key that was meant, such as `camel.trace.enabled` for 
`camel.tracing.enabled`,
+`camel.main.profile` for `camel.profile`, and `camel.component.kafka.brokers` 
for `camel.kafka.brokers`.
+`camel.logging.level` is reported with `logging.level.root`, where the log 
level is set. Any other key that
+is near no Camel key is left alone, as `camel.` can also start a property of 
your own, such as
+`camel.kafka.topic.orders` used as `{{camel.kafka.topic.orders}}` in a route.
+
 `camel validate source` runs the same check on the `.properties` files it is 
given.
 
 == Normalizing YAML routes
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
index d5f15e75042a..8bca3919aa8c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/Ask.java
@@ -125,6 +125,8 @@ public class Ask extends CamelCommand {
     String lastAnswer;
     // the model that answered, and its API, such as qwen3.6 (ollama)
     String usedModel;
+    // the mistakes left in the last answer of a chat, which the model is told 
with the next question (CAMEL-26960)
+    private String pendingNote;
 
     private long targetPid;
     private AskTools askTools;
@@ -245,7 +247,9 @@ public class Ask extends CamelCommand {
             LlmClient client, String systemPrompt,
             List<LlmClient.ToolDef> tools, List<LlmClient.Message> messages,
             String userQuestion) {
-        messages.add(LlmClient.Message.user(userQuestion));
+        String note = pendingNote;
+        pendingNote = null;
+        messages.add(LlmClient.Message.user(note == null ? userQuestion : "[" 
+ note + "]\n" + userQuestion));
 
         long startTime = System.currentTimeMillis();
         LlmClient.TokenUsage totalUsage = LlmClient.TokenUsage.EMPTY;
@@ -290,6 +294,12 @@ public class Ask extends CamelCommand {
                             "(the answer has an invalid simple expression: 
asking for a corrected answer)");
                     continue;
                 }
+                // the code of the answer is checked too: what does not exist, 
and a simple expression left invalid,
+                // is printed under the answer, and in a chat the model is 
told with the next question (CAMEL-26960)
+                List<AnswerChecks.Mark> marks = 
AnswerChecks.checkCode(response.text());
+                marks.forEach(m -> printer().printDiagnostic("Check: " + 
m.describe()));
+                problems.forEach(p -> printer().printDiagnostic("Check: 
invalid simple expression " + p.message()));
+                pendingNote = AnswerChecks.historyNote(marks, problems);
                 
messages.add(LlmClient.Message.assistantWithToolCalls(response.text(), 
List.of()));
                 lastAnswer = response.text();
                 printStats(totalUsage, startTime);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AnswerChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AnswerChecks.java
index 441fcf280785..ab88cda9f200 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AnswerChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AnswerChecks.java
@@ -17,16 +17,22 @@
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 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;
 
+import com.networknt.schema.Error;
 import org.apache.camel.catalog.CamelCatalog;
 import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.catalog.LanguageValidationResult;
+import org.apache.camel.dsl.yaml.validator.ProjectFacts;
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
 import org.apache.camel.tooling.model.LanguageModel;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -41,6 +47,11 @@ import org.slf4j.LoggerFactory;
  * date, ...), so a Maven {@code ${camel-version}} or a shell variable in the 
same answer is left alone. What the
  * catalog cannot judge is skipped as in {@link SimpleChecks}: a function of a 
language that is not on the classpath, a
  * property placeholder used as a logical operand.
+ * <p>
+ * The code blocks of an answer are checked for what does not exist 
(CAMEL-26960): the keys and options of YAML routes
+ * and of fragments of them ({@link YamlFragment}), the options of their 
endpoint uris, and the {@code camel.*} keys of
+ * properties lines. A model told to look options up and to validate its 
snippets wrote more of them and validated none,
+ * so the check does not wait to be asked. Its mistakes are marked under their 
block, not sent back for a correction.
  */
 public final class AnswerChecks {
 
@@ -53,6 +64,46 @@ public final class AnswerChecks {
         }
     }
 
+    /**
+     * A mistake in the code of an answer: a key, an option or a {@code 
camel.*} property that does not exist, which a
+     * user who copies the code takes into a route. block is the number of the 
fenced code block in the answer, 1 for
+     * the first; line is the line in that block, 1 for the line after the 
fence, or 0 when it is not known.
+     */
+    public record Mark(int block, int line, String message) {
+
+        /** {@code code block 2, line 4: `multiplier` is not an option of 
`redeliveryPolicy`}. */
+        public String describe() {
+            return "code block " + block + (line > 0 ? ", line " + line : "") 
+ ": " + message;
+        }
+    }
+
+    /** A fenced code block of an answer: its number (1 for the first), 
language, code and the line of its end fence. */
+    private record CodeBlock(int number, String language, String code, int 
closingLine) {
+    }
+
+    /** A camel.* key at the start of a properties line: 
camel.server.port=8080. */
+    private static final Pattern CAMEL_PROPERTY = 
Pattern.compile("^\\s*(camel\\.[\\w.\\[\\]-]+)\\s*=");
+
+    /** The camel.* keys of the CLI and of other runtimes, which the catalog 
does not know. */
+    private static final List<String> OTHER_PROPERTIES = 
List.of("camel.jbang.", "camel.springboot.", "camel.k.");
+
+    /** What the properties check says of a key that does not exist, and the 
hint after it. */
+    private static final Pattern UNKNOWN_PROPERTY = Pattern.compile("Unknown 
(option|component|dataformat|language)(.*)$");
+
+    /** What the endpoint check says of an option the component does not have: 
line, scheme, option, the hint after. */
+    private static final Pattern UNKNOWN_ENDPOINT_OPTION
+            = Pattern.compile("^Line (\\d+): ([^:\\s]+): Unknown option 
'([^']+)'(.*)$");
+
+    /**
+     * A value of the wrong type, as the schema says it: what was found, and 
the hint of the validator if it has one.
+     */
+    private static final Pattern TYPE_FOUND = Pattern.compile("^(\\w+) found, 
\\w+ expected(?: \\((.+)\\))?$");
+
+    /** The catalog's suggestions, and the words after them. */
+    private static final Pattern DID_YOU_MEAN = Pattern.compile("^\\.?\\s*Did 
you mean: \\[([^]]*)]\\.?(.*)$");
+
+    private static final Pattern UNKNOWN_KEY = Pattern.compile("property 
'([^']+)'");
+
     private static final Logger LOG = 
LoggerFactory.getLogger(AnswerChecks.class);
 
     private static volatile CamelCatalog defaultCatalog;
@@ -147,6 +198,236 @@ public final class AnswerChecks {
         return new ArrayList<>(problems.values());
     }
 
+    /** The mistakes of the code blocks of an answer, checked against the 
default catalog. */
+    public static List<Mark> checkCode(String markdown) {
+        return checkCode(markdown, defaultCatalog());
+    }
+
+    /**
+     * The mistakes of the code blocks of an answer, checked against the given 
catalog: the keys and options of YAML
+     * routes and of fragments of them, the options of their endpoint uris, 
and the camel.* keys of properties lines in
+     * any block. Code in an answer is partial on purpose, so only what does 
not exist is a mistake, not a missing
+     * option, a placeholder value or a fragment shown without its route: a 
mark that is wrong costs the user's trust in
+     * the others.
+     */
+    public static List<Mark> checkCode(String markdown, CamelCatalog catalog) {
+        if (markdown == null || markdown.isBlank() || catalog == null) {
+            return List.of();
+        }
+        List<Mark> marks = new ArrayList<>();
+        for (CodeBlock block : codeBlocks(markdown)) {
+            try {
+                marks.addAll(propertyMarks(block, catalog));
+                if (block.language().equals("yaml") || 
block.language().equals("yml")) {
+                    marks.addAll(yamlMarks(block, catalog));
+                }
+            } catch (Exception e) {
+                // best effort, as for the simple expressions: the cause is in 
the debug log
+                LOG.debug("Cannot check code block {} of the answer", 
block.number(), e);
+            }
+        }
+        return marks;
+    }
+
+    /**
+     * The answer with its marks: under each code block with a mistake, one 
line per mark, indented as the fence is, so
+     * the user sees it where the code is. The code is left as it is, so 
copying a code block copies only the code.
+     */
+    public static String withMarks(String markdown, List<Mark> marks) {
+        if (markdown == null || marks == null || marks.isEmpty()) {
+            return markdown;
+        }
+        Map<Integer, Integer> blockEndingAt = new HashMap<>();
+        for (CodeBlock block : codeBlocks(markdown)) {
+            blockEndingAt.put(block.closingLine(), block.number());
+        }
+        String[] lines = markdown.split("\n", -1);
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < lines.length; i++) {
+            sb.append(lines[i]);
+            Integer number = blockEndingAt.get(i);
+            List<Mark> here = number == null ? List.of() : 
marks.stream().filter(m -> m.block() == number).toList();
+            if (!here.isEmpty()) {
+                String indent = lines[i].substring(0, lines[i].indexOf('`'));
+                sb.append('\n');
+                for (Mark mark : here) {
+                    sb.append('\n').append(indent).append("āœ— 
").append(mark.line() > 0 ? "line " + mark.line() + ": " : "")
+                            .append(mark.message());
+                }
+                if (i + 1 < lines.length && !lines[i + 1].isBlank()) {
+                    sb.append('\n');
+                }
+            }
+            if (i < lines.length - 1) {
+                sb.append('\n');
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * What the model is told with the next question about the mistakes left 
in its last answer, the marks of its code
+     * and the simple expressions it did not fix, so that its next answer does 
not repeat them; null when there are
+     * none. The user sees them under the answer already.
+     */
+    public static String historyNote(List<Mark> marks, List<Problem> problems) 
{
+        List<String> found = new ArrayList<>();
+        if (marks != null) {
+            marks.forEach(m -> found.add(m.describe()));
+        }
+        if (problems != null) {
+            problems.forEach(p -> found.add("invalid simple expression " + 
p.message()));
+        }
+        return found.isEmpty()
+                ? null
+                : "Checked against the Camel catalog, your last answer has 
mistakes, which the user sees marked: "
+                  + String.join("; ", found) + ". Do not repeat them.";
+    }
+
+    /** The fenced code blocks of an answer; one whose fence is not closed 
(the model ran out of tokens) is left out. */
+    private static List<CodeBlock> codeBlocks(String markdown) {
+        List<CodeBlock> answer = new ArrayList<>();
+        String[] lines = markdown.split("\n", -1);
+        String language = null;
+        StringBuilder code = null;
+        for (int i = 0; i < lines.length; i++) {
+            String trimmed = lines[i].trim();
+            if (trimmed.startsWith("```")) {
+                if (code == null) {
+                    String tag = 
trimmed.substring(3).trim().toLowerCase(Locale.ROOT);
+                    language = tag.contains(" ") ? tag.substring(0, 
tag.indexOf(' ')) : tag;
+                    code = new StringBuilder();
+                } else {
+                    answer.add(new CodeBlock(answer.size() + 1, language, 
code.toString(), i));
+                    code = null;
+                }
+            } else if (code != null) {
+                code.append(lines[i]).append('\n');
+            }
+        }
+        return answer;
+    }
+
+    /**
+     * The camel.* keys of the properties lines of a block that the catalog 
does not know, in a block of any language: a
+     * model writes application.properties lines in a yaml block too.
+     */
+    private static List<Mark> propertyMarks(CodeBlock block, CamelCatalog 
catalog) {
+        List<Mark> marks = new ArrayList<>();
+        String[] lines = block.code().split("\n", -1);
+        for (int i = 0; i < lines.length; i++) {
+            Matcher key = CAMEL_PROPERTY.matcher(lines[i]);
+            if (!key.find() || 
OTHER_PROPERTIES.stream().anyMatch(key.group(1)::startsWith)) {
+                continue;
+            }
+            String msg = 
PropertiesChecks.validatePropertyLine(lines[i].strip(), catalog, null);
+            Matcher unknown = msg != null ? UNKNOWN_PROPERTY.matcher(msg) : 
null;
+            if (unknown != null && unknown.find()) {
+                String what = unknown.group(1).equals("option")
+                        ? "a Camel option" : "a Camel option: there is no such 
" + unknown.group(1);
+                marks.add(new Mark(block.number(), i + 1, "`" + key.group(1) + 
"` is not " + what + hint(unknown.group(2))));
+            }
+        }
+        return marks;
+    }
+
+    /** The keys, options and endpoint options of a YAML block that do not 
exist; nothing when it is not Camel YAML. */
+    private static List<Mark> yamlMarks(CodeBlock block, CamelCatalog catalog) 
throws Exception {
+        YamlFragment fragment = YamlFragment.of(block.code());
+        if (fragment == null) {
+            return List.of();
+        }
+        List<Mark> marks = new ArrayList<>();
+        for (Error error : 
SourceValidator.yamlValidator(catalog).validate(fragment.content(), 
ProjectFacts.NONE)) {
+            Mark mark = schemaMark(block.number(), fragment, error);
+            if (mark != null) {
+                marks.add(mark);
+            }
+        }
+        for (String msg : 
EndpointChecks.validateYamlEndpoints(fragment.content(), catalog)) {
+            Matcher m = UNKNOWN_ENDPOINT_OPTION.matcher(msg);
+            int line = m.matches() ? Integer.parseInt(m.group(1)) - 
fragment.shift() : 0;
+            if (line > 0) {
+                marks.add(new Mark(
+                        block.number(), line,
+                        "`" + m.group(3) + "` is not an option of the `" + 
m.group(2) + "` component" + hint(m.group(4))));
+            }
+        }
+        return marks;
+    }
+
+    /**
+     * The mark of an error of the schema, or null when it is no mistake of 
the block: an unknown key where the wrapping
+     * put the block, a required key it leaves out, a value of the wrong type 
the validator has no hint for (a
+     * placeholder often is one).
+     */
+    private static Mark schemaMark(int block, YamlFragment fragment, Error 
error) {
+        String location = String.valueOf(error.getInstanceLocation());
+        String message = error.getMessage();
+        if (message == null || fragment.items().matcher(location).matches()) {
+            return null;
+        }
+        String text = null;
+        if ("additionalProperties".equals(error.getKeyword())) {
+            Matcher key = UNKNOWN_KEY.matcher(message);
+            int after = message.indexOf("additional properties");
+            if (key.find()) {
+                text = "`" + key.group(1) + "` is not " + owner(location)
+                       + (after >= 0 ? hint(message.substring(after + 
"additional properties".length())) : "");
+            }
+        } else if ("type".equals(error.getKeyword())) {
+            // a hint of the validator in place of the message (expression: 
holds the language as its key, ...) or after
+            // it (onException is a map, not a list, ...); a bare "string 
found, object expected" is left out, and so is
+            // null, the value of a key a fragment leaves empty (steps: with a 
comment under it)
+            Matcher found = TYPE_FOUND.matcher(message);
+            text = !found.matches() ? message : "null".equals(found.group(1)) 
? null : found.group(2);
+        }
+        if (text == null) {
+            return null;
+        }
+        return new Mark(block, 
Math.max(YamlValidator.lineOf(fragment.content(), error) - fragment.shift(), 
0), text);
+    }
+
+    /** What an unknown key is not a part of: an option of the EIP at the 
location, or an EIP for an item of steps. */
+    private static String owner(String location) {
+        String[] parts = location.split("/");
+        String last = parts.length > 0 ? parts[parts.length - 1] : "";
+        if (!last.matches("\\d+")) {
+            return "an option of `" + last + "`";
+        }
+        String list = parts.length > 1 ? parts[parts.length - 2] : "";
+        return switch (list) {
+            case "steps" -> "an EIP";
+            case "beans" -> "a key of a bean";
+            default -> "a key of the items of `" + list + "`";
+        };
+    }
+
+    /**
+     * The hint after what a check says, as a mark writes it: " (did you mean 
`delete`?)" for the catalog's ". Did you
+     * mean: [delete]", the check's own words in parentheses, or nothing.
+     */
+    private static String hint(String rest) {
+        // the schema's "did you mean 'backOffMultiplier'?" as code, as the 
names of the mark are
+        String text = rest == null ? "" : rest.strip().replaceAll("did you 
mean '([\\w.-]+)'\\?", "did you mean `$1`?");
+        Matcher dym = DID_YOU_MEAN.matcher(text);
+        if (dym.matches()) {
+            List<String> names = new ArrayList<>();
+            for (String name : dym.group(1).split(",")) {
+                if (!name.isBlank()) {
+                    names.add("`" + name.strip() + "`");
+                }
+            }
+            String more = dym.group(2).strip();
+            return (names.isEmpty() ? "" : " (did you mean " + String.join(" 
or ", names) + "?)")
+                   + (more.isEmpty() ? "" : " " + more);
+        }
+        if (text.isEmpty()) {
+            return "";
+        }
+        return text.startsWith("(") ? " " + text : " (" + 
text.replaceFirst("^[.,:;]\\s*", "") + ")";
+    }
+
     /**
      * The {@code ${...}} placeholders of a text, braces balanced so {@code 
${header.${header.key}}} is one placeholder,
      * in the order they appear.
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 f065e545f227..44bac54ce083 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
@@ -187,7 +187,9 @@ final class EndpointChecks {
                     int mapIndent = -1;
                     for (int k = j + 1; k < lines.length; k++) {
                         String paramLine = lines[k];
-                        if (paramLine.isBlank()) {
+                        if (paramLine.isBlank() || 
paramLine.trim().startsWith("#")) {
+                            // a commented-out option is not one: "# 
idempotentRepository: ..." was reported as the
+                            // unknown option '# idempotentRepository' 
(CAMEL-26960)
                             continue;
                         }
                         int paramIndent = countLeadingSpaces(paramLine);
@@ -477,7 +479,12 @@ final class EndpointChecks {
             Map.entry("timer:cron", "a cron expression is the cron or quartz 
component: cron:tick?schedule=0/5+*+*+*+*+?"),
             Map.entry("timer:schedule", "a cron expression is the cron or 
quartz component: cron:tick?schedule=0/5+*+*+*+*+?"),
             Map.entry("log:message", "the message is the body; a text is set 
with a setBody step or the log EIP"),
-            Map.entry("log:name", "the logger name is the path: 
log:com.example"));
+            Map.entry("log:name", "the logger name is the path: 
log:com.example"),
+            // CAMEL-26960: 
jdbc:mysql://localhost/orders?user=...&password=..., a JDBC url written as the 
endpoint
+            Map.entry("jdbc:user", "jdbc: takes the name of a DataSource bean, 
jdbc:myDataSource; the JDBC url, user and"
+                                   + " password are set on that DataSource"),
+            Map.entry("jdbc:password", "jdbc: takes the name of a DataSource 
bean, jdbc:myDataSource; the JDBC url, user"
+                                       + " and password are set on that 
DataSource"));
 
     /** direct:a,direct:b or log:a,mock:b: a comma followed by another scheme 
inside one uri. */
     static final Pattern SEVERAL_ENDPOINTS_PATTERN
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/PropertiesChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/PropertiesChecks.java
index d580618cfae4..c58fb24b127b 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/PropertiesChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/PropertiesChecks.java
@@ -17,6 +17,8 @@
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
 import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
@@ -154,12 +156,126 @@ final class PropertiesChecks {
                 }
                 return null;
             }
+            // camel.tracing.enabled: a group Camel does not have, which the 
catalog leaves to the caller
+            String unknown = unknownGroupHint(line, catalog);
+            if (unknown != null) {
+                return unknown;
+            }
         } catch (Exception e) {
             // ignore validation errors
         }
         return extra != null ? extra.apply(line) : null;
     }
 
+    /**
+     * The camel.* prefixes Camel reads besides the groups of the main model: 
in camel-main, the keys the user names
+     * (beans, variables, Kamelets, route templates), the options of the 
CamelContext and the dev consoles as they are,
+     * and the global options; the prefixes of components 
(camel.kubernetes-config, camel.oauth, camel.knative,
+     * camel.wolf-defender), the virtual threads 
(camel.threads.virtual.enabled), the resource loader and the
+     * customizers; and the keys of the Camel CLI, its TUI, MCP server, 
launcher and test infra, and the defaults of its
+     * commands (camel.run.dev). Without dashes and in lower case, as a group 
is compared.
+     */
+    private static final Set<String> OTHER_GROUPS = Set.of("component", 
"dataformat", "language", "beans", "variable",
+            "kamelet", "routetemplate", "context", "devconsole", 
"globaloptions", "kubernetesconfig", "oauth", "knative",
+            "wolfdefender", "threads", "resource", "customizer", "jbang", 
"tui", "cli", "mcp", "catalog", "launcher",
+            "infra", "run", "export");
+
+    /** Where the log level is set, for a camel.* key that tries to set it. */
+    private static final String LOG_LEVEL
+            = "the log level is set with logging.level.root=INFO, or 
logging.level.<package>=DEBUG for one package";
+
+    /** The camel.* groups of other runtimes: the Camel Spring Boot starters 
and Camel K. */
+    private static final Set<String> OTHER_RUNTIME_GROUPS = 
Set.of("springboot", "servlet", "cloud", "openapi",
+            "springdoc", "observation", "micrometer", "clustered", 
"mcpserver", "k");
+
+    /**
+     * camel.tracing.enabled=true: a key of a group Camel does not have. 
camel-main puts each camel.* key in its group
+     * and drops one of a group it does not know without a word, so the 
tracing the line was meant to turn on stays off.
+     * The catalog leaves such a key to the caller, as it judges only its own 
groups (CAMEL-26960).
+     * <p>
+     * Said only when the key that was meant is near: the option in a group of 
a near name (camel.trace.enabled), a dash
+     * for the dot of a group (camel.metrics-enabled: camel.metrics.enabled), 
an option of camel.main (camel.profile:
+     * camel.main.profile), an option of a component without component. 
(camel.kafka.brokers), or a group of a near
+     * name; and a log level (camel.logging.level), which is set with 
logging.level.root. A key that is near none of
+     * them is left alone: camel. is also a prefix of keys of the user's own, 
such as camel.kafka.topic.logs used as
+     * {{camel.kafka.topic.logs}} in a route. Null as well when the group is 
one Camel reads.
+     */
+    static String unknownGroupHint(String line, CamelCatalog catalog) {
+        String key = line.contains("=") ? line.substring(0, 
line.indexOf('=')).trim() : line.trim();
+        MainModel mm = catalog.mainModel();
+        if (!key.startsWith("camel.") || mm == null) {
+            return null;
+        }
+        String rest = key.substring("camel.".length());
+        int dot = rest.indexOf('.');
+        String group = dot > 0 ? rest.substring(0, dot) : rest;
+        String after = dot > 0 ? rest.substring(dot) : "";
+        Set<String> options = new HashSet<>();
+        Map<String, String> groups = new LinkedHashMap<>();
+        for (var o : mm.getOptions()) {
+            options.add(normalized(o.getName()));
+            String name = o.getName().substring(o.getName().indexOf('.') + 1);
+            String first = name.contains(".") ? name.substring(0, 
name.indexOf('.')) : name;
+            groups.putIfAbsent(normalized(first), first);
+        }
+        // camel.route-template[0].name: the index of an item of the group is 
not part of its name
+        String unknown = normalized(group.replaceFirst("\\[.*", ""));
+        if (unknown.isEmpty() || groups.containsKey(unknown) || 
OTHER_GROUPS.contains(unknown)
+                || OTHER_RUNTIME_GROUPS.contains(unknown)) {
+            return null;
+        }
+        if (unknown.matches("log(ging)?(level)?") && (after.isEmpty() || 
normalized(after).contains("level"))) {
+            // camel.logging.level=INFO: the log level is a logging.level.* 
key, near no Camel key
+            return group + "    Unknown option (camel." + group + " is not a 
group of Camel options; " + LOG_LEVEL + ")";
+        }
+        String meant = null;
+        String dotted = "camel." + group.replaceFirst("-", ".") + after;
+        if (group.contains("-") && options.contains(normalized(dotted))) {
+            meant = dotted;
+        }
+        List<String> near = groups.values().stream().filter(g -> 
isNear(unknown, normalized(g)))
+                .sorted(Comparator.comparingInt(g -> editDistance(unknown, 
normalized(g)))).toList();
+        for (String g : near) {
+            if (meant == null && dot > 0 && 
options.contains(normalized("camel." + g + after))) {
+                meant = "camel." + g + after;
+            }
+        }
+        if (meant == null && dot < 0 && 
options.contains(normalized("camel.main." + group))) {
+            meant = "camel.main." + group;
+        }
+        if (meant == null && dot > 0 && isComponentOption(catalog, group, 
rest.substring(dot + 1))) {
+            meant = "camel.component." + group + after;
+        }
+        if (meant == null && !near.isEmpty()) {
+            meant = "camel." + near.get(0) + ".*";
+        }
+        return meant == null
+                ? null
+                : group + "    Unknown option (camel." + group + " is not a 
group of Camel options; did you mean " + meant
+                  + "?)";
+    }
+
+    /** Whether the option is one of the component, as a 
camel.component.name.option key sets it. */
+    private static boolean isComponentOption(CamelCatalog catalog, String 
component, String option) {
+        var model = catalog.findComponentNames().contains(component) ? 
catalog.componentModel(component) : null;
+        return model != null && model.getComponentOptions().stream()
+                .anyMatch(o -> 
normalized(o.getName()).equals(normalized(option)));
+    }
+
+    /** A group name as it is compared: camel-main reads the keys without 
regard to case, and dashes as camel case. */
+    private static String normalized(String name) {
+        return name.replace("-", "").toLowerCase(Locale.ROOT);
+    }
+
+    /** Whether a group name is near another: one starts the other, they start 
with the same four letters, or a typo. */
+    private static boolean isNear(String unknown, String group) {
+        int same = 0;
+        while (same < unknown.length() && same < group.length() && 
unknown.charAt(same) == group.charAt(same)) {
+            same++;
+        }
+        return unknown.startsWith(group) || group.startsWith(unknown) || same 
>= 4 || editDistance(unknown, group) <= 2;
+    }
+
     /**
      * For an unknown camel.main.* key, the closest real option name 
(camel.main.duration -> durationMaxSeconds). For an
      * option that exists in another group of the main configuration, that 
group (camel.component.netty-http
@@ -180,8 +296,7 @@ final class PropertiesChecks {
         }
         if (main && 
camel.toLowerCase(Locale.ROOT).matches("log(ging)?level|log(ging)?")) {
             // camel.main.loggingLevel=INFO: the log level is a 
logging.level.* key, not a camel.* option
-            return "(the log level is set with logging.level.root=INFO, or 
logging.level.<package>=DEBUG for one"
-                   + " package)";
+            return "(" + LOG_LEVEL + ")";
         }
         try {
             List<String> known = new ArrayList<>();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlFragment.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlFragment.java
new file mode 100644
index 000000000000..a910be2ccd00
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/YamlFragment.java
@@ -0,0 +1,145 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A YAML code block of an AI answer, made into something the YAML DSL schema 
can check (CAMEL-26960). The code of an
+ * answer is mostly a fragment: a {@code from:} without its route, a list of 
steps, one EIP. Checked as a route file,
+ * the schema refuses its shape before it looks at its keys, so the fragment 
is wrapped in what it is a part of: an
+ * entry of a route file goes in a file, steps and an EIP go in a route, and 
an option of one EIP (redeliveryPolicy:)
+ * goes under that EIP. An option map without its key (slidingWindowSize: 10) 
is not checked: what it belongs to is not
+ * known.
+ *
+ * @param content the YAML the schema checks: the block as it is for a route 
file, else the block in its wrapping
+ * @param shift   the lines the wrapping puts before the first line of the 
block, to subtract from a line of an error
+ * @param items   the locations of the items that hold the block: an unknown 
key there says only that the block is not
+ *                what it was taken for (options of an EIP shown without the 
EIP: slidingWindowSize: 10), so it is not a
+ *                mistake of the block
+ */
+record YamlFragment(String content, int shift, Pattern items) {
+
+    /** The key of the first line: an item of a list (- from:) or a key of a 
mapping (from:). */
+    private static final Pattern FIRST_KEY = Pattern.compile("^(- 
)?([A-Za-z_][\\w.-]*)\\s*:");
+
+    /** The top-level keys of YAML that is not Camel: docker-compose, 
Kubernetes, Spring Boot and Quarkus settings. */
+    private static final Set<String> OTHER_YAML = Set.of("apiVersion", "kind", 
"metadata", "services", "version",
+            "spring", "quarkus", "camel", "server", "management", "logging", 
"jobs", "on");
+
+    /** The route that steps and an EIP are put in: they are checked as the 
steps of its from:. */
+    private static final String ROUTE = "- route:\n    from:\n      uri: 
direct:answer\n      steps:\n";
+
+    private static final Pattern FILE_ITEMS = Pattern.compile("/\\d+");
+    private static final Pattern ENTRY_ITEMS = Pattern.compile("/0");
+    private static final Pattern STEP_ITEMS = 
Pattern.compile("/0(/route(/from(/steps/\\d+)?)?)?");
+
+    /**
+     * The keys that are neither an entry nor a step but an option of one EIP, 
which a model shows on their own
+     * (redeliveryPolicy: with its options), and the wrapping that puts them 
under that EIP: what goes before the block,
+     * how far the block is indented, and the locations of the wrapping.
+     */
+    private record Parent(String before, String indent, Pattern items) {
+    }
+
+    private static final Parent ON_EXCEPTION = new Parent(
+            "- onException:\n    exception:\n      - java.lang.Exception\n", " 
   ",
+            Pattern.compile("/0(/onException(/exception(/\\d+)?)?)?"));
+
+    private static final Parent CIRCUIT_BREAKER = new Parent(
+            ROUTE + "        - circuitBreaker:\n", "            ",
+            
Pattern.compile("/0(/route(/from(/steps(/0(/circuitBreaker)?)?)?)?)?"));
+
+    private static final Map<String, Parent> PARENTS = Map.of(
+            "redeliveryPolicy", ON_EXCEPTION,
+            "resilience4jConfiguration", CIRCUIT_BREAKER,
+            "faultToleranceConfiguration", CIRCUIT_BREAKER,
+            "onFallback", CIRCUIT_BREAKER);
+
+    /**
+     * The block made checkable, or null when it is not Camel YAML: it has no 
key, or its first key is one of another
+     * tool (services:, apiVersion:, spring:).
+     */
+    static YamlFragment of(String code) {
+        String[] lines = code.split("\n", -1);
+        int indent = Integer.MAX_VALUE;
+        String first = null;
+        for (String line : lines) {
+            if (!line.isBlank() && !line.strip().startsWith("#")) {
+                indent = Math.min(indent, YamlLines.countLeadingSpaces(line));
+                if (first == null) {
+                    first = line;
+                }
+            }
+        }
+        if (first == null) {
+            return null;
+        }
+        // a block cut out of a deeper level of a route: its lines as if it 
were at the top
+        String[] dedented = new String[lines.length];
+        for (int i = 0; i < lines.length; i++) {
+            dedented[i] = YamlLines.countLeadingSpaces(lines[i]) >= indent ? 
lines[i].substring(indent) : lines[i].strip();
+        }
+        Matcher m = FIRST_KEY.matcher(first.substring(indent));
+        if (!m.find() || OTHER_YAML.contains(m.group(2))) {
+            return null;
+        }
+        boolean item = m.group(1) != null;
+        if (CatalogSamples.TOP_LEVEL.contains(m.group(2))) {
+            // a route file (- route:, - beans:, - onException:), or one of 
its entries written as a mapping (from:)
+            return item
+                    ? new YamlFragment(String.join("\n", dedented), 0, 
FILE_ITEMS)
+                    : new YamlFragment(indented(dedented, "- ", "  "), 0, 
ENTRY_ITEMS);
+        }
+        Parent parent = item ? null : PARENTS.get(m.group(2));
+        if (parent != null) {
+            // redeliveryPolicy: under an onException, 
resilience4jConfiguration: under a circuitBreaker
+            return new YamlFragment(
+                    parent.before() + indented(dedented, parent.indent(), 
parent.indent()),
+                    (int) parent.before().chars().filter(c -> c == 
'\n').count(), parent.items());
+        }
+        // steps (- to:, - log:), or one EIP (idempotentConsumer:): the steps 
of a route
+        return item
+                ? new YamlFragment(ROUTE + indented(dedented, "        ", "    
    "), 4, STEP_ITEMS)
+                : new YamlFragment(ROUTE + indented(dedented, "        - ", "  
        "), 4, STEP_ITEMS);
+    }
+
+    /**
+     * The lines with first in front of the first line that has a key and 
other in front of the others, so a mapping
+     * becomes an item of a list, or a list moves in under the key that holds 
it. The lines stay where they are, so a
+     * line of the content is a line of the block plus {@link #shift()}.
+     */
+    private static String indented(String[] lines, String first, String other) 
{
+        StringBuilder sb = new StringBuilder();
+        boolean keyed = false;
+        for (int i = 0; i < lines.length; i++) {
+            String line = lines[i];
+            if (!line.isEmpty()) {
+                boolean key = !keyed && !line.isBlank() && 
!line.strip().startsWith("#");
+                sb.append(key ? first : other).append(line);
+                keyed |= key;
+            }
+            if (i < lines.length - 1) {
+                sb.append('\n');
+            }
+        }
+        return sb.toString();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/AskLoopTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/AskLoopTest.java
index 0f591b63b636..d686ffa9e5ae 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/AskLoopTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/AskLoopTest.java
@@ -107,6 +107,20 @@ class AskLoopTest extends CamelCommandBaseTestSupport {
         assertThat(printer.getStderr()).contains("Reached maximum iterations 
(2) without a final answer.");
     }
 
+    @Test
+    void theMistakesOfTheCodeOfTheAnswerAreSaidUnderIt() throws Exception {
+        // CAMEL-26960: an option that does not exist, on stderr, so the 
answer stays the only output on stdout
+        String text = "Delete the files after reading 
them:\n\n```yaml\nfrom:\n  uri: file:orders\n  parameters:\n"
+                      + "    deleteAfterRead: true\n```\n";
+        server.reply("/api/chat", 200, answer(text));
+
+        assertThat(command().doCall()).isZero();
+
+        assertThat(printer.getStdout()).isEqualTo(text.strip());
+        assertThat(printer.getStderr()).contains("Check: code block 1, line 4: 
`deleteAfterRead` is not an option of"
+                                                 + " the `file` component (did 
you mean `delete`?)");
+    }
+
     @Test
     void theStatsOnStderr() throws Exception {
         server.reply("/api/chat", 200, answer("The routes are fine"));
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AnswerCodeChecksTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AnswerCodeChecksTest.java
new file mode 100644
index 000000000000..5fa9c51141c9
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AnswerCodeChecksTest.java
@@ -0,0 +1,265 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.util.List;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.jbang.core.commands.ai.AnswerChecks.Mark;
+import org.apache.camel.dsl.jbang.core.commands.ai.AnswerChecks.Problem;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.tuple;
+
+/**
+ * CAMEL-26960: the code blocks of an answer are checked for what does not 
exist, and the mistakes are marked under
+ * their block. The snippets are from a benchmark with a local model 
(qwen3.6:35b, November 2026), which wrote them in
+ * its answers to beginner questions.
+ */
+class AnswerCodeChecksTest {
+
+    private static final CamelCatalog CATALOG = new DefaultCamelCatalog();
+
+    @Test
+    void aFromWithoutItsRouteIsChecked() {
+        List<Mark> marks = AnswerChecks.checkCode(answer("yaml", """
+                from:
+                  uri: file:orders
+                  steps:
+                    - idempotentConsumer:
+                        simple: "${header.CamelFileName}"
+                        messageIdRepository: orderIds
+                        steps:
+                          - to: direct:audit
+                """), CATALOG);
+        assertThat(marks).extracting(Mark::block, 
Mark::line).containsExactly(tuple(1, 6));
+        assertThat(marks.get(0).describe())
+                .startsWith("code block 1, line 6: `messageIdRepository` is 
not an option of `idempotentConsumer`");
+    }
+
+    @Test
+    void stepsAndOneEipAreCheckedInARoute() {
+        List<Mark> marks = AnswerChecks.checkCode(answer("yaml", """
+                - circuitBreaker:
+                    forceOpen: false
+                    failureThreshold: 50
+                    steps:
+                      - to: direct:supplier
+                """), CATALOG);
+        assertThat(marks).extracting(Mark::line).containsExactly(2, 3);
+        assertThat(marks.get(0).message()).isEqualTo("`forceOpen` is not an 
option of `circuitBreaker`");
+        // the validator's hint comes with the mark
+        assertThat(marks.get(1).message()).startsWith("`failureThreshold` is 
not an option of `circuitBreaker` (")
+                .contains("resilience4jConfiguration");
+
+        marks = AnswerChecks.checkCode(answer("yaml", """
+                idempotentConsumer:
+                  simple: "${body[orderId]}"
+                  messageIdRepository: orderIds
+                """), CATALOG);
+        assertThat(marks).extracting(Mark::line).containsExactly(3);
+    }
+
+    @Test
+    void anOptionOfOneEipIsCheckedUnderIt() {
+        List<Mark> marks = AnswerChecks.checkCode(answer("yaml", """
+                redeliveryPolicy:
+                  maximumRedeliveries: 3
+                  redeliveryDelay: 1000
+                  multiplier: 2          # 1s, 2s, 4s
+                  useExponentialBackOff: true
+                """), CATALOG);
+        assertThat(marks).extracting(Mark::line).containsExactly(4);
+        assertThat(marks.get(0).message()).startsWith("`multiplier` is not an 
option of `redeliveryPolicy`");
+    }
+
+    @Test
+    void theOptionsOfAnEndpointAreChecked() {
+        List<Mark> marks = AnswerChecks.checkCode(answer("yaml", """
+                from:
+                  uri: file:orders
+                  parameters:
+                    noop: true
+                    deleteAfterRead: true
+                  steps:
+                    - to: "direct:audit?retryCount=3"
+                """), CATALOG);
+        assertThat(marks).extracting(Mark::line, 
Mark::message).containsExactly(
+                tuple(5, "`deleteAfterRead` is not an option of the `file` 
component (did you mean `delete`?)"),
+                tuple(7, "`retryCount` is not an option of the `direct` 
component"));
+
+        // an option that is commented out is not one
+        assertThat(AnswerChecks.checkCode(answer("yaml", """
+                from:
+                  uri: file
+                  parameters:
+                    directoryName: orders
+                    # idempotentRepository: "#redisRepo"
+                  steps:
+                    - log: "${body}"
+                """), CATALOG)).isEmpty();
+    }
+
+    @Test
+    void theCamelPropertiesAreCheckedInAnyBlock() {
+        // a model writes application.properties lines in a yaml block too
+        List<Mark> marks = AnswerChecks.checkCode(answer("yaml", """
+                # Add to application.properties
+                camel.server.context-path=/api
+                camel.jbang.dependencies=org.example:orders:1.0
+                orders.directory=orders
+                camel.tracing.enabled=true
+                camel.kafka.topic.logs=otlp_logs
+                """), CATALOG);
+        assertThat(marks).extracting(Mark::line).containsExactly(2, 5);
+        
assertThat(marks.get(0).message()).startsWith("`camel.server.context-path` is 
not a Camel option (")
+                .contains("camel.rest.contextPath");
+        // a group Camel does not have, which camel-main drops without a word; 
the key of the user's own is left alone
+        assertThat(marks.get(1).message()).startsWith("`camel.tracing.enabled` 
is not a Camel option (")
+                .contains("did you mean camel.trace.enabled?");
+
+        marks = AnswerChecks.checkCode(answer("properties", """
+                camel.main.name=orders
+                camel.component.file.orders-directory=orders
+                """), CATALOG);
+        assertThat(marks).extracting(Mark::line, Mark::message)
+                .containsExactly(tuple(2, 
"`camel.component.file.orders-directory` is not a Camel option"));
+    }
+
+    @Test
+    void anOnExceptionWrittenAsAListIsMarked() {
+        // the exception class as a key: the validator knows the mistake, and 
says how to write it
+        List<Mark> marks = AnswerChecks.checkCode(answer("yaml", """
+                onException:
+                  - java.lang.Exception:
+                      handled: true
+                      steps:
+                        - to: direct:error-handler
+                """), CATALOG);
+        assertThat(marks).extracting(Mark::line).containsExactly(2);
+        assertThat(marks.get(0).message()).startsWith("onException is a map, 
not a list");
+    }
+
+    @Test
+    void whatIsNotAMistakeIsNotMarked() {
+        // a route as the examples have it
+        assertThat(marks("yaml", """
+                - route:
+                    id: orders
+                    from:
+                      uri: timer:orders?period=5000
+                      steps:
+                        - setBody:
+                            simple: "order ${random(1000)}"
+                        - log: "${body}"
+                """)).isEmpty();
+        // options shown without the EIP they are of: what they belong to is 
not known
+        assertThat(marks("yaml", """
+                slidingWindowSize: 10
+                minimumNumberOfCalls: 5
+                """)).isEmpty();
+        // a required option and the steps left out, and a placeholder
+        assertThat(marks("yaml", """
+                - from:
+                    uri: timer
+                    steps:
+                      - to: "{{orders.target}}"
+                      - choice:
+                          otherwise:
+                            steps:
+                              # your steps here
+                """)).isEmpty();
+        // the YAML of other tools
+        assertThat(marks("yaml", """
+                services:
+                  kafka:
+                    image: quay.io/strimzi/kafka:latest
+                """)).isEmpty();
+        assertThat(marks("yaml", """
+                apiVersion: v1
+                kind: ConfigMap
+                metadata:
+                  name: orders
+                """)).isEmpty();
+        assertThat(marks("yaml", """
+                camel:
+                  springboot:
+                    name: orders
+                """)).isEmpty();
+        // a message body, and a block of another language
+        assertThat(marks("yaml", """
+                "lines": [{"sku": "CAMEL-TSHIRT", "qty": 1}]
+                """)).isEmpty();
+        assertThat(marks("text", """
+                redeliveryPolicy:
+                  multiplier: 2
+                """)).isEmpty();
+        // a block the model did not finish
+        assertThat(AnswerChecks.checkCode("Retry 
with:\n```yaml\nredeliveryPolicy:\n  multiplier: 2\n", CATALOG))
+                .isEmpty();
+    }
+
+    @Test
+    void theMarksGoUnderTheirBlock() {
+        String answer = """
+                Two changes:
+
+                1. Keep the order files:
+
+                   ```yaml
+                   from:
+                     uri: file:orders?noop=true
+                   ```
+                2. Retry the call:
+
+                   ```yaml
+                   redeliveryPolicy:
+                     multiplier: 2
+                   ```
+                That is all.""";
+        List<Mark> marks = AnswerChecks.checkCode(answer, CATALOG);
+        assertThat(marks).extracting(Mark::block, 
Mark::line).containsExactly(tuple(2, 2));
+
+        String marked = AnswerChecks.withMarks(answer, marks);
+        // the first block is as it was, and the mark of the second is under 
it, indented as its fence
+        assertThat(marked).startsWith(answer.substring(0, answer.indexOf("2. 
Retry")));
+        assertThat(marked).endsWith("     multiplier: 2\n   ```\n\n"
+                                    + "   āœ— line 2: `multiplier` is not an 
option of `redeliveryPolicy`\n\nThat is all.");
+        assertThat(AnswerChecks.withMarks(answer, 
List.of())).isEqualTo(answer);
+    }
+
+    @Test
+    void theModelIsToldWhatIsLeft() {
+        Mark mark = new Mark(2, 4, "`multiplier` is not an option of 
`redeliveryPolicy`");
+        Problem problem = new Problem("${header.user ?: Guest}", "Other 
operator ?: does not accept Guest");
+        assertThat(AnswerChecks.historyNote(List.of(mark), List.of(problem)))
+                .contains("code block 2, line 4: `multiplier` is not an option 
of `redeliveryPolicy`")
+                .contains("invalid simple expression ${header.user ?: Guest}: 
Other operator ?: does not accept Guest")
+                .endsWith("Do not repeat them.");
+        assertThat(AnswerChecks.historyNote(List.of(), List.of())).isNull();
+    }
+
+    private static List<Mark> marks(String language, String code) {
+        return AnswerChecks.checkCode(answer(language, code), CATALOG);
+    }
+
+    private static String answer(String language, String code) {
+        return "Here is how:\n\n```" + language + "\n" + code + "```\n\nThat 
should do it.";
+    }
+}
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 61b5a6f5f4f7..178c299a7f9f 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
@@ -102,6 +102,39 @@ class SourceValidatorEndpointTest {
         assertThat(errors.get(0)).containsIgnoringCase("unknown");
     }
 
+    @Test
+    void aCommentedOutOptionIsNotAnOption() {
+        // CAMEL-26960: was the unknown option '# idempotentRepository'
+        String yaml = """
+                - from:
+                    uri: file
+                    parameters:
+                      directoryName: orders
+                      noop: true
+                      # or
+                      # idempotentRepository: "#redisRepo"
+                    steps:
+                      - log: "${body}"
+                """;
+        assertThat(SourceValidator.validateYamlEndpoints(yaml, 
catalog)).isEmpty();
+    }
+
+    @Test
+    void aJdbcUrlWrittenAsTheEndpointSaysWhatJdbcTakes() {
+        // CAMEL-26960: a benchmark answer wrote the JDBC url of the database 
as the uri of the jdbc component
+        String yaml = """
+                - from:
+                    uri: timer:tick
+                    steps:
+                      - to: 
jdbc:mysql://localhost/orders?user=app&password=secret
+                """;
+        List<String> errors = SourceValidator.validateYamlEndpoints(yaml, 
catalog);
+        assertThat(errors).hasSize(2);
+        assertThat(errors.get(0)).startsWith("Line 4: jdbc: Unknown option 
'user'")
+                .contains("jdbc: takes the name of a DataSource bean, 
jdbc:myDataSource");
+        assertThat(errors.get(1)).startsWith("Line 4: jdbc: Unknown option 
'password'");
+    }
+
     @Test
     void expandedFormInvalidBoolean() {
         String yaml = """
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorPropertiesTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorPropertiesTest.java
index b894b3ce1eb9..69d9523fccad 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorPropertiesTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorPropertiesTest.java
@@ -204,4 +204,62 @@ public class SourceValidatorPropertiesTest {
         assertThat(msgs.get(0)).contains("Unknown option").contains("period is 
an endpoint option of timer")
                 .contains("timer:name?period=...");
     }
+
+    /**
+     * CAMEL-26960: a key of a group Camel does not have, which camel-main 
drops without a word, with the key that was
+     * meant.
+     */
+    @Test
+    void aKeyOfAGroupCamelDoesNotHaveSaysWhichKeyWasMeant() {
+        List<String> msgs = SourceValidator.validateProperties("""
+                camel.tracing.enabled=true
+                camel.profile=production
+                camel.metrics-enabled=true
+                camel.kafka.brokers=localhost:9092
+                camel.mian.name=orders
+                camel.opentelemtry.enabled=true
+                """, catalog, null);
+        assertThat(msgs).hasSize(6);
+        assertThat(msgs.get(0)).isEqualTo("Line 1: tracing    Unknown option 
(camel.tracing is not a group of Camel"
+                                          + " options; did you mean 
camel.trace.enabled?)");
+        assertThat(msgs.get(1)).endsWith("did you mean camel.main.profile?)");
+        assertThat(msgs.get(2)).endsWith("did you mean 
camel.metrics.enabled?)");
+        assertThat(msgs.get(3)).endsWith("did you mean 
camel.component.kafka.brokers?)");
+        assertThat(msgs.get(4)).endsWith("did you mean camel.main.name?)");
+        assertThat(msgs.get(5)).endsWith("did you mean 
camel.opentelemetry.enabled?)");
+    }
+
+    @Test
+    void aLogLevelOfACamelLoggingKeyPointsToLoggingLevel() {
+        // CAMEL-26960: near no Camel key, so the near-key hints of a group 
Camel does not have say nothing
+        List<String> msgs = SourceValidator.validateProperties("""
+                camel.logging.level=INFO
+                camel.log-level=DEBUG
+                camel.logging.pattern=%d %m%n
+                """, catalog, null);
+        assertThat(msgs).hasSize(2);
+        assertThat(msgs.get(0)).isEqualTo("Line 1: logging    Unknown option 
(camel.logging is not a group of Camel"
+                                          + " options; the log level is set 
with logging.level.root=INFO, or"
+                                          + " logging.level.<package>=DEBUG 
for one package)");
+        assertThat(msgs.get(1)).startsWith("Line 2: log-level    Unknown 
option").contains("logging.level.root=INFO");
+    }
+
+    @Test
+    void theKeysCamelReadsBesidesItsGroupsAndTheKeysOfTheUserAreLeftAlone() {
+        assertThat(SourceValidator.validateProperties("""
+                
camel.beans.orderIds=#class:org.apache.camel.support.processor.idempotent.MemoryIdempotentRepository
+                camel.variable.greeting=Hello
+                camel.kamelet.my-source.period=5000
+                camel.route-template[0].template-id=myTemplate
+                camel.jbang.dependencies=org.example:orders:1.0
+                camel.mcp.security.redaction.enabled=true
+                camel.tui.theme=light
+                camel.routeController.enabled=true
+                camel.kubernetes-config.mount-path-secrets=/etc/camel/secrets/
+                camel.threads.virtual.enabled=true
+                camel.servlet.mapping.context-path=/api
+                camel.kafka.topic.logs=otlp_logs
+                camel.myapp.greeting=hello
+                """, catalog, null)).isEmpty();
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
index 2efc52689ff3..cddd71a2f664 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateTools.java
@@ -189,7 +189,7 @@ public class PropertiesTranslateTools {
             "camel.metrics.",
             "camel.opentelemetry.",
             "camel.opentelemetry2.",
-            "camel.tracing.",
+            "camel.trace.",
             "camel.threadpool.",
             "camel.resilience4j.",
             "camel.faulttolerance.",
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateToolsTest.java
index 42ca2adf9c7d..a85a9300e83b 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/PropertiesTranslateToolsTest.java
@@ -160,6 +160,16 @@ class PropertiesTranslateToolsTest {
         });
     }
 
+    @Test
+    void theTracerKeysAreCamelKeysAndCamelTracingIsNot() {
+        // CAMEL-26960: camel.trace is the group of the tracer; camel.tracing 
is no group, which Camel ignores
+        PropertiesTranslateTools.PropertiesTranslateResult result = 
tools.camel_properties_translate(
+                "camel.trace.enabled=true\ncamel.tracing.enabled=true", 
"main", "spring-boot");
+
+        assertThat(result.properties().get(0).status()).isEqualTo("unchanged");
+        assertThat(result.properties().get(1).status()).isEqualTo("unknown");
+    }
+
     @Test
     void sameRuntimeReturnsKnownKeysUnchanged() {
         PropertiesTranslateTools.PropertiesTranslateResult result = 
tools.camel_properties_translate(
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
index 715e7068533d..02267a7c5765 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanel.java
@@ -1434,14 +1434,20 @@ class AiPanel {
         if (text != null && !text.isBlank()) {
             long elapsed = MonotonicClock.millis() - thinkingStartTime;
             String note = problems.isEmpty() ? null : answerNote(problems);
+            // the code of the answer is checked too: what does not exist is 
marked under its block, and the model is
+            // told of it, and of a simple expression left invalid, with the 
next question (CAMEL-26960)
+            List<AnswerChecks.Mark> marks = AnswerChecks.checkCode(text);
+            String told = AnswerChecks.historyNote(marks, problems);
+            pendingNote = told == null ? pendingNote : pendingNote == null ? 
told : pendingNote + "\n" + told;
+            String shown = AnswerChecks.withMarks(text, marks);
             ConversationEntry entry = new ConversationEntry(
-                    AiRole.ASSISTANT, text, elapsed, turnAiMs, turnToolMs, 
turnToolCalls,
+                    AiRole.ASSISTANT, shown, elapsed, turnAiMs, turnToolMs, 
turnToolCalls,
                     totalUsage.totalTokens(), turnRequests, wrapUp, 
contextFillPercent(), corrections, note);
             conversation.add(entry);
             usage.addTurn(turnAiMs, turnToolMs, turnToolCalls);
             String label = wrapUp ? "Response after reaching the tool call 
limit (" : "Response (";
             log(LogLevel.RESPONSE, label + entry.byline() + 
describeCacheSignal(totalUsage) + ")",
-                    note == null ? text : text + "\n\n" + note);
+                    note == null ? shown : shown + "\n\n" + note);
         } else {
             String err = "Empty response from LLM.";
             conversation.add(new ConversationEntry(AiRole.ERROR, err));
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
index 1f50075211ab..00770014ff65 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java
@@ -1200,6 +1200,69 @@ class AiPanelTest {
         assertFalse(last.byline().contains("correction"), last.byline());
     }
 
+    // ---- the code of an answer is checked, and what does not exist is 
marked (CAMEL-26960) ----
+
+    @Test
+    void theMistakesOfTheCodeOfAnAnswerAreMarkedAndToldWithTheNextQuestion() 
throws Exception {
+        AiPanel panel = new AiPanel();
+        panel.setToolRegistryForTesting(new TuiToolRegistry(null));
+        MarkedLlmClient client = new MarkedLlmClient();
+        panel.setClientForTesting(client);
+        panel.open();
+        type(panel, "how do I retry the call");
+        panel.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
!panel.isAgentThreadRunningForTesting());
+
+        AiPanel.ConversationEntry last = 
panel.conversationForTesting().get(panel.conversationForTesting().size() - 1);
+        assertEquals(AiRole.ASSISTANT, last.role(), last.text());
+        assertTrue(last.text().contains("```\n\nāœ— line 4: `multiplier` is not 
an option of `redeliveryPolicy`"),
+                last.text());
+        assertEquals(1, client.answers, "a mark is not a correction turn: the 
model is not asked again");
+        assertEquals(0, last.corrections());
+
+        // the model is told with the next question; its answer stays in the 
history as it wrote it
+        type(panel, "and what else");
+        panel.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE));
+        await().atMost(10, TimeUnit.SECONDS).until(() -> 
!panel.isAgentThreadRunningForTesting());
+        assertTrue(client.lastQuestion.contains("[Checked against the Camel 
catalog, your last answer has mistakes"),
+                client.lastQuestion);
+        assertTrue(client.lastQuestion.contains("code block 1, line 4: 
`multiplier` is not an option of"),
+                client.lastQuestion);
+        assertTrue(client.lastQuestion.endsWith("and what else"), 
client.lastQuestion);
+        assertEquals(MarkedLlmClient.ANSWER, client.previousAnswer);
+        assertTrue(panel.conversationForTesting().stream().noneMatch(e -> 
e.role() == AiRole.SYSTEM),
+                "the marks are under the answer already, so no line says that 
the model is told");
+    }
+
+    /** Answers with a redelivery policy that has an option no redelivery 
policy has, and keeps what it is sent. */
+    private static final class MarkedLlmClient extends LlmClient {
+
+        static final String ANSWER = "Retry three 
times:\n\n```yaml\nredeliveryPolicy:\n  maximumRedeliveries: 3\n"
+                                     + "  redeliveryDelay: 1000\n  multiplier: 
2\n```\n";
+
+        volatile int answers;
+        volatile String lastQuestion;
+        volatile String previousAnswer;
+
+        MarkedLlmClient() {
+            withModel("test-model");
+            withApiType(ApiType.openai);
+        }
+
+        @Override
+        public boolean detectEndpoint() {
+            return true;
+        }
+
+        @Override
+        public ChatResponse chatWithTools(String systemPrompt, List<Message> 
messages, List<ToolDef> tools) {
+            answers++;
+            lastQuestion = messages.get(messages.size() - 1).content();
+            previousAnswer = messages.size() >= 2 ? 
messages.get(messages.size() - 2).content() : null;
+            return new ChatResponse(answers == 1 ? ANSWER : "Nothing else.", 
List.of(), "stop", false, TokenUsage.EMPTY);
+        }
+    }
+
     /** Answers with a fallback that is not quoted, and fixes it when asked, 
like a small local model. */
     private static final class CorrectingLlmClient extends LlmClient {
 
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 2fe2487f41a4..080e10afbda4 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
@@ -135,6 +135,12 @@ final class SchemaHints {
 
     private static final Predicate<Match> ANY = m -> true;
 
+    /** Whether the node of the error, an item of beans, has its name: key. */
+    private static boolean hasName(Match m) {
+        JsonNode instance = m.error().getInstanceNode();
+        return instance != null && instance.has("name");
+    }
+
     /** {sku} when the node is the map YAML reads an unquoted {sku} as (one 
key, no value); null otherwise. */
     static String unquotedBraces(Match m) {
         JsonNode instance = m.error().getInstanceNode();
@@ -870,8 +876,14 @@ final class SchemaHints {
             unknownProperty(".*/beans/\\d+", m -> Set.of("id", "ref", 
"class").contains(m.unknown()),
                     m -> "a bean is - name: myBean followed by type: 
\"#class:com.example.MyBean\" (name instead of "
                          + m.unknown() + (m.unknown().equals("class") ? ", 
type instead of class" : "") + ")"),
-            // - myBean: {type: ...} instead of - name: myBean / type: ...
-            unknownProperty(".*/beans/\\d+", ANY,
+            // - name: repo / type: ... / parameters: {...}: the options of a 
bean are its properties (CAMEL-26960)
+            unknownProperty(".*/beans/\\d+",
+                    m -> hasName(m) && Set.of("parameters", "params", 
"options", "args").contains(m.unknown()),
+                    m -> "what a bean is set up with goes under properties: 
(its setters, properties: {fileStore: x})"
+                         + " or constructors: (the arguments of its 
constructor, constructors: {0: x})"),
+            // - myBean: {type: ...} instead of - name: myBean / type: ...; 
not for an item that has its name, where the
+            // key is one a bean does not have
+            unknownProperty(".*/beans/\\d+", m -> !hasName(m),
                     m -> "a bean item is written as - name: " + m.unknown()
                          + " followed by type: \"#class:com.example.MyBean\" 
(the name is a property, not the key)"),
             // errorHandler: under from: belongs to the route; told only that 
it is a top-level entry, models moved a
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 fc5aeb0052c5..8ad5674c90a5 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
@@ -686,6 +686,31 @@ public class YamlValidatorPropertyHintTest {
         assertThat(errors).anyMatch(e -> e.getMessage().contains("a bean item 
is written as - name: myBean"));
     }
 
+    @Test
+    public void testBeanWithItsNameAndAnUnknownKeyIsNotTakenForAKeyedItem() 
throws Exception {
+        // CAMEL-26960: a local model wrote the setters of a bean under 
parameters:, and was told that its name is a
+        // property and not the key, which it already was
+        List<Error> errors = validator.validate("""
+                - beans:
+                    - name: orderIds
+                      type: 
org.apache.camel.processor.idempotent.FileIdempotentRepository
+                      parameters:
+                        fileStore: orders.dat
+                """);
+        assertThat(errors).anyMatch(e -> e.getMessage().contains("property 
'parameters' is not defined")
+                && e.getMessage().contains("goes under properties:"));
+        assertThat(errors).noneMatch(e -> e.getMessage().contains("a bean item 
is written as"));
+
+        errors = validator.validate("""
+                - beans:
+                    - name: orderIds
+                      type: 
org.apache.camel.processor.idempotent.FileIdempotentRepository
+                      fileStore: orders.dat
+                """);
+        assertThat(errors).anyMatch(e -> e.getMessage().contains("property 
'fileStore' is not defined"));
+        assertThat(errors).noneMatch(e -> e.getMessage().contains("a bean item 
is written as"));
+    }
+
     @Test
     public void testTemplateBeansKeyedByNameSayHowToWriteThem() throws 
Exception {
         // CAMEL-24704: the beans of a routeTemplate and a templatedRoute are 
the same list as the top-level beans

Reply via email to