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

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

commit 6a81a3d8f30e2b1435fa4f59ea9904f8498333a0
Author: Luigi De Masi <[email protected]>
AuthorDate: Thu Sep 24 08:53:16 2026 +0200

    CAMEL-24977: Fix semantic reload and address review feedback
    
    Remove definitions from deleted question resources before development 
reload.
    Initialize adapter transport during evaluation and publish evaluation fields
    with explicit visibility. Preserve Camel-owned probability decision policy.
    
    Fix AsciiDoc expressions, complete named-question editor metadata, remove 
the
    unsupported YAML alias, and add JAXB/YAML round-trip and error-path 
coverage.
    Regenerate documentation and completion catalog mirrors.
    
    Co-authored-by: Codex <[email protected]>
    Signed-off-by: Luigi De Masi <[email protected]>
---
 .../camel/catalog/docs/semantic-language.adoc      | 11 ++--
 .../camel/catalog/schemas/camelYamlDsl-model.json  | 39 +++++++++++---
 components/camel-ai/camel-semantic/pom.xml         |  5 --
 .../src/main/docs/semantic-language.adoc           | 11 ++--
 .../camel/language/semantic/SemanticLanguage.java  | 16 ++++--
 .../apache/camel/semantic/SemanticQuestions.java   | 23 ++++++++-
 .../camel/semantic/SemanticReloadPlugin.java       | 36 +++++++++++++
 .../org.apache.camel.spi.ContextServicePlugin      |  1 +
 .../camel/semantic/SemanticLanguageTest.java       | 40 ++++++++++++++-
 .../camel/semantic/SemanticPropertiesTest.java     |  5 +-
 .../apache/camel/semantic/SemanticResultTest.java  | 53 +++++++++++++++++++
 .../typesafeai/TypeSafeAiSemanticAdapter.java      | 10 +++-
 .../typesafeai/TypeSafeAiSemanticAdapterTest.java  | 34 ++++++++++++
 .../camel/spring/ChoiceSelectorJaxbTest.java       | 55 ++++++++++++++++++++
 .../apache/camel/processor/ChoiceSelectorTest.java | 18 +++++++
 .../jbang/core/commands/tui/SourceEditAssist.java  | 33 +++++++++++-
 .../dsl/jbang/core/commands/tui/SourceViewer.java  |  5 +-
 .../jbang/core/commands/tui/YamlSourceContext.java | 22 ++++++++
 .../core/commands/tui/SemanticCompletionTest.java  | 60 ++++++++++++++++++++++
 .../SemanticDefinitionDeserializer.java            |  8 +--
 .../maven/dsl/yaml/GenerateYamlCompletionMojo.java | 53 +++++++++++++++++--
 .../dsl/yaml/validator/SemanticSchemaTest.java     | 11 ++--
 dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml          |  5 ++
 .../resources/schema/camelYamlDsl-model.json       | 39 +++++++++++---
 .../camel/dsl/yaml/YamlRoutesBuilderLoader.java    |  4 +-
 .../camel/dsl/yaml/ChoiceSelectorYamlDumpTest.java | 46 +++++++++++++++++
 .../camel/dsl/yaml/SemanticQuestionTest.java       | 48 +++++++++++++++--
 27 files changed, 631 insertions(+), 60 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
index 033656bf5df9..f55e366bb74d 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/semantic-language.adoc
@@ -23,7 +23,7 @@ concurrency limits and transport resources. TypeSafe AI uses
 
 One advertised adapter is selected automatically. No provider, or multiple 
distinct
 providers, is an error. Select an existing bean with
-`camel.language.semantic.adapter=#bean:myAdapter`, or a class using its fully 
qualified name
+`camel.language.semantic.adapter=#bean:myAdapter` (or `#myAdapter`), or a 
class using its fully qualified name
 (with an optional `#class:` prefix). Class selection uses Camel's class 
resolver and injector.
 Created adapters are registered as `camelSemanticAdapter` and managed by the 
context.
 A collision at that name is an error. Referenced beans retain their existing 
lifecycle owner.
@@ -85,18 +85,23 @@ The YAML DSL supports declarations alongside routes, 
including declarations afte
 
 Names are context-wide. Duplicate declarations across resources and unknown 
references fail.
 Reloading a resource replaces its complete set of questions, including 
removing declarations
-no longer present. This replacement does not make the surrounding route reload 
transactional.
+no longer present. Development-mode route reload also removes definitions from 
deleted or renamed files
+before parsing replacements. This replacement does not make the surrounding 
route reload transactional.
 Existing expressions resolve the current definition on their next evaluation.
 Loading declarations does not perform inference. Java applications can 
register immutable
 `SemanticQuestion` definitions using 
`SemanticQuestions.get(context).replace(source, questions)`.
 
 A question's optional `state` Simple expression overrides
-`camel.language.semantic.default-state`, whose default is `${body}`. Selectors 
are compiled
+`camel.language.semantic.default-state`, whose default is `$\{body}`. 
Selectors are compiled
 before evaluation; selected strings, maps and lists are passed as data and are 
never evaluated recursively.
 A missing selected header fails instead of falling back to the body. Blank or 
invalid selectors
 fail. The original message is preserved. `CamelSemanticResult` contains the 
latest successful
 normalized result and is cleared before each evaluation, including one that 
fails.
 
+State must be a string, map or list. For byte arrays or stream bodies, 
explicitly select
+`$\{bodyAs(String)}`. Enable stream caching before evaluating a stream when 
later processors
+also need to read it. Unsupported state types fail instead of being implicitly 
converted.
+
 == Results and policy
 
 Only boolean questions can be predicates. A category string is never 
implicitly a boolean.
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
index 9a2023971bc9..7f813a5f8ecf 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
@@ -863,13 +863,11 @@
       } ]
     },
     "semantic" : {
-      "title" : "Semantic",
-      "description" : "Evaluate named semantic questions through a provider 
adapter",
-      "label" : "language,ai",
       "isListItem" : true,
       "children" : [ {
         "name" : "question",
-        "type" : "object",
+        "type" : "map",
+        "ref" : "semanticDefinitionDeserializer$QuestionSchema",
         "required" : true
       } ]
     },
@@ -921,7 +919,32 @@
       } ]
     },
     "semanticDefinitionDeserializer$QuestionSchema" : {
-      "children" : [ ]
+      "children" : [ {
+        "name" : "criteria",
+        "type" : "any"
+      }, {
+        "name" : "instructions",
+        "type" : "string",
+        "required" : true
+      }, {
+        "name" : "state",
+        "type" : "string"
+      }, {
+        "name" : "threshold",
+        "type" : "number"
+      }, {
+        "name" : "type",
+        "type" : "enum",
+        "enum" : [ "boolean", "choice", "score" ],
+        "required" : true
+      }, {
+        "name" : "uncertainty",
+        "type" : "number"
+      }, {
+        "name" : "uncertaintyPolicy",
+        "type" : "enum",
+        "enum" : [ "fail", "non-match" ]
+      } ]
     },
     "semanticDefinitionDeserializer$ScoreSchema" : {
       "children" : [ {
@@ -1374,9 +1397,9 @@
       } ]
     },
     "bean" : {
-      "title" : "Bean Method",
-      "description" : "Calls a Java bean method",
-      "label" : "language,core,java",
+      "title" : "Bean",
+      "description" : "Invokes a method on a Java bean, with automatic 
parameter binding from the message body, headers, and exchange properties",
+      "label" : "eip,endpoint",
       "input" : true,
       "isListItem" : true,
       "children" : [ {
diff --git a/components/camel-ai/camel-semantic/pom.xml 
b/components/camel-ai/camel-semantic/pom.xml
index e04a54bed4bd..0efc5678a0a9 100644
--- a/components/camel-ai/camel-semantic/pom.xml
+++ b/components/camel-ai/camel-semantic/pom.xml
@@ -47,11 +47,6 @@
             <artifactId>camel-main</artifactId>
             <scope>test</scope>
         </dependency>
-        <dependency>
-            <groupId>org.apache.camel</groupId>
-            <artifactId>camel-xml-io-dsl</artifactId>
-            <scope>test</scope>
-        </dependency>
         <dependency>
             <groupId>org.assertj</groupId>
             <artifactId>assertj-core</artifactId>
diff --git 
a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc 
b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
index 033656bf5df9..f55e366bb74d 100644
--- a/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
+++ b/components/camel-ai/camel-semantic/src/main/docs/semantic-language.adoc
@@ -23,7 +23,7 @@ concurrency limits and transport resources. TypeSafe AI uses
 
 One advertised adapter is selected automatically. No provider, or multiple 
distinct
 providers, is an error. Select an existing bean with
-`camel.language.semantic.adapter=#bean:myAdapter`, or a class using its fully 
qualified name
+`camel.language.semantic.adapter=#bean:myAdapter` (or `#myAdapter`), or a 
class using its fully qualified name
 (with an optional `#class:` prefix). Class selection uses Camel's class 
resolver and injector.
 Created adapters are registered as `camelSemanticAdapter` and managed by the 
context.
 A collision at that name is an error. Referenced beans retain their existing 
lifecycle owner.
@@ -85,18 +85,23 @@ The YAML DSL supports declarations alongside routes, 
including declarations afte
 
 Names are context-wide. Duplicate declarations across resources and unknown 
references fail.
 Reloading a resource replaces its complete set of questions, including 
removing declarations
-no longer present. This replacement does not make the surrounding route reload 
transactional.
+no longer present. Development-mode route reload also removes definitions from 
deleted or renamed files
+before parsing replacements. This replacement does not make the surrounding 
route reload transactional.
 Existing expressions resolve the current definition on their next evaluation.
 Loading declarations does not perform inference. Java applications can 
register immutable
 `SemanticQuestion` definitions using 
`SemanticQuestions.get(context).replace(source, questions)`.
 
 A question's optional `state` Simple expression overrides
-`camel.language.semantic.default-state`, whose default is `${body}`. Selectors 
are compiled
+`camel.language.semantic.default-state`, whose default is `$\{body}`. 
Selectors are compiled
 before evaluation; selected strings, maps and lists are passed as data and are 
never evaluated recursively.
 A missing selected header fails instead of falling back to the body. Blank or 
invalid selectors
 fail. The original message is preserved. `CamelSemanticResult` contains the 
latest successful
 normalized result and is cleared before each evaluation, including one that 
fails.
 
+State must be a string, map or list. For byte arrays or stream bodies, 
explicitly select
+`$\{bodyAs(String)}`. Enable stream caching before evaluating a stream when 
later processors
+also need to read it. Unsupported state types fail instead of being implicitly 
converted.
+
 == Results and policy
 
 Only boolean questions can be predicates. A category string is never 
implicitly a boolean.
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
index a940c6be673b..9eefad836867 100644
--- 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/language/semantic/SemanticLanguage.java
@@ -113,8 +113,12 @@ public class SemanticLanguage extends LanguageSupport {
         }
         CamelContext context = getCamelContext();
         String configured = adapter == null ? null : 
context.resolvePropertyPlaceholders(adapter);
-        if (configured != null && configured.startsWith("#bean:")) {
-            Object bean = 
context.getRegistry().lookupByName(configured.substring(6));
+        if (configured != null && configured.startsWith("#") && 
!configured.startsWith("#class:")) {
+            String name = configured.startsWith("#bean:") ? 
configured.substring(6) : configured.substring(1);
+            if (name.contains(":")) {
+                throw new IllegalArgumentException("Semantic adapter reference 
must use #bean:name, #name or #class:FQCN");
+            }
+            Object bean = context.getRegistry().lookupByName(name);
             if (!(bean instanceof SemanticAdapter found)) {
                 throw new IllegalArgumentException("Semantic adapter bean is 
missing or does not implement SemanticAdapter");
             }
@@ -212,8 +216,8 @@ public class SemanticLanguage extends LanguageSupport {
         private final String name;
         private final boolean predicate;
         private volatile Compiled compiled;
-        private SemanticQuestions questions;
-        private SemanticAdapter provider;
+        private volatile SemanticQuestions questions;
+        private volatile SemanticAdapter provider;
 
         private Evaluation(String name, boolean predicate) {
             this.name = name;
@@ -264,7 +268,9 @@ public class SemanticLanguage extends LanguageSupport {
                     throw new IllegalArgumentException("Missing selected state 
for semantic question: " + name);
                 }
                 if (!(state instanceof String || state instanceof Map<?, ?> || 
state instanceof List<?>)) {
-                    throw new IllegalArgumentException("Unsupported state type 
for semantic question: " + name);
+                    throw new IllegalArgumentException(
+                            "Unsupported state type for semantic question: " + 
name
+                                                       + ". Select strings, 
maps or lists explicitly");
                 }
                 SemanticResult result = provider.evaluate(question, state);
                 if (result == null) {
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java
index b35a698eff31..5a67f7b63e15 100644
--- 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticQuestions.java
@@ -20,10 +20,12 @@ import java.util.HashMap;
 import java.util.Map;
 
 import org.apache.camel.CamelContext;
+import org.apache.camel.spi.Resource;
 
 /** Context-local named questions, replaced atomically per source when a route 
resource is reloaded. */
 public final class SemanticQuestions {
     private final Map<String, Map<String, SemanticQuestion>> sources = new 
HashMap<>();
+    private final Map<String, Resource> resources = new HashMap<>();
     private volatile Map<String, SemanticQuestion> questions = Map.of();
 
     public static SemanticQuestions get(CamelContext context) {
@@ -53,10 +55,29 @@ public final class SemanticQuestions {
                 throw new IllegalArgumentException("Duplicate semantic 
question: " + name);
             }
         });
-        sources.put(source, Map.copyOf(definitions));
+        if (definitions.isEmpty()) {
+            sources.remove(source);
+        } else {
+            sources.put(source, Map.copyOf(definitions));
+        }
+        resources.remove(source);
         questions = Map.copyOf(replacement);
     }
 
+    /** Track a route resource so deleted files can be discarded before 
development-mode reload. */
+    public synchronized void replace(Resource source, Map<String, 
SemanticQuestion> definitions) {
+        removeDeletedResources();
+        replace(source.getLocation(), definitions);
+        if (!definitions.isEmpty() && "file".equals(source.getScheme())) {
+            resources.put(source.getLocation(), source);
+        }
+    }
+
+    synchronized void removeDeletedResources() {
+        resources.values().stream().filter(resource -> 
!resource.exists()).map(Resource::getLocation).toList()
+                .forEach(location -> replace(location, Map.of()));
+    }
+
     public SemanticQuestion get(String name) {
         SemanticQuestion question = questions.get(name);
         if (question == null) {
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java
new file mode 100644
index 000000000000..95a4ba3d1342
--- /dev/null
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/SemanticReloadPlugin.java
@@ -0,0 +1,36 @@
+/*
+ * 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.semantic;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.spi.ContextServicePlugin;
+
+/** Removes definitions from deleted files before the route watcher loads 
their replacements. */
+public class SemanticReloadPlugin implements ContextServicePlugin {
+    @Override
+    public void load(CamelContext context) {
+        // Questions are registered by the route loader or the application.
+    }
+
+    @Override
+    public void onReload(CamelContext context) {
+        SemanticQuestions questions = 
context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class);
+        if (questions != null) {
+            questions.removeDeletedResources();
+        }
+    }
+}
diff --git 
a/components/camel-ai/camel-semantic/src/main/resources/META-INF/services/org.apache.camel.spi.ContextServicePlugin
 
b/components/camel-ai/camel-semantic/src/main/resources/META-INF/services/org.apache.camel.spi.ContextServicePlugin
new file mode 100644
index 000000000000..6279fb409835
--- /dev/null
+++ 
b/components/camel-ai/camel-semantic/src/main/resources/META-INF/services/org.apache.camel.spi.ContextServicePlugin
@@ -0,0 +1 @@
+org.apache.camel.semantic.SemanticReloadPlugin
diff --git 
a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticLanguageTest.java
 
b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticLanguageTest.java
index 6dcc4379eb4d..98e1723f5f02 100644
--- 
a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticLanguageTest.java
+++ 
b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticLanguageTest.java
@@ -17,6 +17,7 @@
 package org.apache.camel.semantic;
 
 import java.net.URL;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Collections;
@@ -27,6 +28,7 @@ import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.camel.Expression;
 import org.apache.camel.Predicate;
+import org.apache.camel.RuntimeCamelException;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.impl.engine.DefaultClassResolver;
 import org.apache.camel.language.semantic.SemanticLanguage;
@@ -144,9 +146,14 @@ class SemanticLanguageTest {
     @Test
     void classAndBeanTypeErrorsAndRegistryCollisionDoNotFallBack() {
         language.setAdapter(String.class.getName());
-        assertThatThrownBy(() -> 
language.createExpression("ref:q")).isInstanceOf(Exception.class);
+        assertThatThrownBy(() -> 
language.createExpression("ref:q")).isInstanceOf(RuntimeCamelException.class)
+                .hasCauseInstanceOf(ClassCastException.class);
         language.setAdapter("#bean:missing");
         assertThatThrownBy(() -> 
language.createExpression("ref:q")).hasMessageContaining("missing");
+        context.getRegistry().bind("wrong", "not an adapter");
+        language.setAdapter("#bean:wrong");
+        assertThatThrownBy(() -> language.createExpression("ref:q"))
+                
.isInstanceOf(IllegalArgumentException.class).hasMessageContaining("does not 
implement SemanticAdapter");
         language.setAdapter(CountingAdapter.class.getName());
         context.getRegistry().bind(SemanticLanguage.ADAPTER_NAME, "occupied");
         assertThatThrownBy(() -> 
language.createExpression("ref:q")).hasMessageContaining("already bound");
@@ -189,7 +196,36 @@ class SemanticLanguageTest {
         assertThatThrownBy(() -> 
language.createExpression("ref:unknown")).hasMessageContaining("Unknown");
         questions(
                 question(SemanticQuestion.Type.BOOLEAN, "${invalidFunction}", 
0.5, 0, SemanticQuestion.UncertaintyPolicy.FAIL));
-        assertThatThrownBy(() -> 
language.createExpression("ref:q")).isInstanceOf(Exception.class);
+        assertThatThrownBy(() -> 
language.createExpression("ref:q")).hasMessageContaining("Unknown function: 
invalidFunction");
+    }
+
+    @Test
+    void shorthandBeanReferenceRetainsExistingInstance() {
+        CountingAdapter bean = new CountingAdapter();
+        context.getRegistry().bind("custom", bean);
+        language.setAdapter("#custom");
+        language.createExpression("ref:q");
+        assertThat(CountingAdapter.constructed).hasValue(1);
+        assertThat(CountingAdapter.started).hasValue(0);
+        
assertThat(context.getRegistry().lookupByName(SemanticLanguage.ADAPTER_NAME)).isNull();
+    }
+
+    @Test
+    void byteStateRequiresExplicitConversionAndPreservesBody() {
+        Expression expression = language.createExpression("ref:q");
+        var exchange = new DefaultExchange(context);
+        byte[] body = "invoice".getBytes(StandardCharsets.UTF_8);
+        exchange.getMessage().setBody(body);
+        assertThatThrownBy(() -> expression.evaluate(exchange, Object.class))
+                
.hasCauseInstanceOf(IllegalArgumentException.class).hasMessageContaining("Unsupported
 state type");
+        assertThat(exchange.getProperty(SemanticLanguage.RESULT)).isNull();
+        questions(question(SemanticQuestion.Type.BOOLEAN, "${bodyAs(String)}", 
0.5, 0,
+                SemanticQuestion.UncertaintyPolicy.FAIL));
+        assertThat(expression.evaluate(exchange, Boolean.class)).isTrue();
+        CountingAdapter adapter
+                = 
context.getRegistry().lookupByNameAndType(SemanticLanguage.ADAPTER_NAME, 
CountingAdapter.class);
+        assertThat(adapter.state).isEqualTo("invoice");
+        assertThat(exchange.getMessage().getBody()).isSameAs(body);
     }
 
     @Test
diff --git 
a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticPropertiesTest.java
 
b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticPropertiesTest.java
index 228ce9566b42..d8a829c16850 100644
--- 
a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticPropertiesTest.java
+++ 
b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticPropertiesTest.java
@@ -31,13 +31,14 @@ import static 
org.assertj.core.api.Assertions.assertThatThrownBy;
 
 class SemanticPropertiesTest {
     @ParameterizedTest
-    @ValueSource(strings = { "bean", "class", "plain" })
+    @ValueSource(strings = { "bean", "shorthand", "class", "plain" })
     void camelMainBindsAdapterAndDefaultState(String selection) throws 
Exception {
         Main main = new Main();
         SemanticLanguageTest.LabelAdapter bean = new 
SemanticLanguageTest.LabelAdapter();
         main.bind("classifier", bean);
         String adapter = switch (selection) {
             case "bean" -> "#bean:classifier";
+            case "shorthand" -> "#classifier";
             case "class" -> "#class:" + 
SemanticLanguageTest.LabelAdapter.class.getName();
             default -> SemanticLanguageTest.LabelAdapter.class.getName();
         };
@@ -53,7 +54,7 @@ class SemanticPropertiesTest {
         });
         try {
             main.start();
-            if (selection.equals("bean")) {
+            if (selection.equals("bean") || selection.equals("shorthand")) {
                 
assertThat(main.getCamelContext().getRegistry().lookupByName(SemanticLanguage.ADAPTER_NAME)).isNull();
             } else {
                 
assertThat(main.getCamelContext().getRegistry().lookupByName(SemanticLanguage.ADAPTER_NAME))
diff --git 
a/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticResultTest.java
 
b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticResultTest.java
new file mode 100644
index 000000000000..8d1536aae935
--- /dev/null
+++ 
b/components/camel-ai/camel-semantic/src/test/java/org/apache/camel/semantic/SemanticResultTest.java
@@ -0,0 +1,53 @@
+/*
+ * 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.semantic;
+
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+class SemanticResultTest {
+    @Test
+    void booleanOnlyProviderCannotSilentlyIgnoreProbabilityPolicy() {
+        SemanticResult result = new SemanticResult(true, null, null, null, 
null);
+        SemanticQuestion defaults = 
SemanticLanguageTest.question(SemanticQuestion.Type.BOOLEAN, null, 0.5, 0,
+                SemanticQuestion.UncertaintyPolicy.FAIL);
+        assertThat(result.decision(defaults)).isEqualTo(true);
+        SemanticQuestion threshold = 
SemanticLanguageTest.question(SemanticQuestion.Type.BOOLEAN, null, 0.7, 0,
+                SemanticQuestion.UncertaintyPolicy.FAIL);
+        assertThatThrownBy(() -> 
result.decision(threshold)).isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("decision policy");
+        SemanticQuestion uncertain = 
SemanticLanguageTest.question(SemanticQuestion.Type.BOOLEAN, null, 0.5, 0.1,
+                SemanticQuestion.UncertaintyPolicy.NON_MATCH);
+        assertThatThrownBy(() -> 
result.decision(uncertain)).isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("decision policy");
+    }
+
+    @Test
+    void choiceProbabilitiesMustCoverExactlyTheDeclaredCriteria() {
+        SemanticQuestion question = 
SemanticLanguageTest.question(SemanticQuestion.Type.CHOICE, null, 0.5, 0,
+                SemanticQuestion.UncertaintyPolicy.FAIL);
+        SemanticResult incomplete = new SemanticResult("billing", null, 
Map.of("billing", 0.8), null, null);
+        assertThatThrownBy(() -> 
incomplete.decision(question)).isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("cover every criterion");
+        SemanticResult complete = new SemanticResult("billing", null, 
Map.of("billing", 0.8, "technical", 0.2), null, null);
+        assertThat(complete.decision(question)).isEqualTo("billing");
+    }
+}
diff --git 
a/components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java
 
b/components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java
index 0fde44ed43f7..404d6dd6d355 100644
--- 
a/components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java
+++ 
b/components/camel-ai/camel-typesafe-ai/src/main/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapter.java
@@ -47,17 +47,25 @@ public class TypeSafeAiSemanticAdapter implements 
SemanticAdapter, CamelContextA
                 || question.getType() == SemanticQuestion.Type.SCORE && 
question.getLevels().size() > 10) {
             throw new IllegalArgumentException("TypeSafe AI supports at most 
255 choice criteria or 10 score levels");
         }
+    }
+
+    private TypeSafeAiEndpoint endpoint() {
         if (endpoint == null) {
             synchronized (this) {
                 if (endpoint == null) {
+                    if (camelContext == null) {
+                        throw new IllegalStateException("TypeSafe AI semantic 
adapter requires a CamelContext");
+                    }
                     endpoint = 
camelContext.getEndpoint("typesafe-ai:semantic", TypeSafeAiEndpoint.class);
                 }
             }
         }
+        return endpoint;
     }
 
     @Override
     public SemanticResult evaluate(SemanticQuestion question, Object state) 
throws Exception {
+        validate(question);
         Map<String, Object> definition = new HashMap<>();
         definition.put("instructions", question.getInstructions());
         String type = switch (question.getType()) {
@@ -71,7 +79,7 @@ public class TypeSafeAiSemanticAdapter implements 
SemanticAdapter, CamelContextA
         } else if (!question.getCriteria().isEmpty()) {
             definition.put("criteria", question.getCriteria());
         }
-        JsonObject response = endpoint.evaluate(Map.of("state", state, 
"questions", Map.of("question", definition)));
+        JsonObject response = endpoint().evaluate(Map.of("state", state, 
"questions", Map.of("question", definition)));
         JsonObject answer = 
response.getJsonObject("answers").getJsonObject("question");
         Map<String, Double> probabilities = new HashMap<>();
         if (answer.get("probabilities") instanceof Map<?, ?> values) {
diff --git 
a/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
 
b/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
index d4dd2a2c599f..ac90da5eb6e9 100644
--- 
a/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
+++ 
b/components/camel-ai/camel-typesafe-ai/src/test/java/org/apache/camel/component/typesafeai/TypeSafeAiSemanticAdapterTest.java
@@ -18,6 +18,8 @@ package org.apache.camel.component.typesafeai;
 
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
 
 import org.apache.camel.Expression;
 import org.apache.camel.language.semantic.SemanticLanguage;
@@ -31,6 +33,38 @@ import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 class TypeSafeAiSemanticAdapterTest extends TypeSafeAiTestSupport {
+    @Test
+    void directEvaluationInitializesTransportWithoutPriorValidation() throws 
Exception {
+        respond = request -> result(Map.of("question", Map.of("type", "noul", 
"noul", 0.9)));
+        TypeSafeAiSemanticAdapter adapter = new TypeSafeAiSemanticAdapter();
+        adapter.setCamelContext(context);
+        SemanticQuestion question = new SemanticQuestion(
+                SemanticQuestion.Type.BOOLEAN, "Classify", null,
+                Map.of(), List.of(), 0.5, 0, 
SemanticQuestion.UncertaintyPolicy.FAIL);
+        assertThat(adapter.evaluate(question, 
"original").decision(question)).isEqualTo(true);
+        assertThat(requests).hasSize(1);
+        assertThat(authorization).containsExactly("Bearer test-key");
+    }
+
+    @Test
+    void rejectsCapabilityLimitsWithoutInitializingTransport() {
+        TypeSafeAiSemanticAdapter adapter = new TypeSafeAiSemanticAdapter();
+        Map<String, String> criteria = IntStream.range(0, 256).boxed()
+                .collect(Collectors.toMap(Object::toString, i -> "Criterion " 
+ i));
+        SemanticQuestion choice = new SemanticQuestion(
+                SemanticQuestion.Type.CHOICE, "Classify", null,
+                criteria, List.of(), 0.5, 0, 
SemanticQuestion.UncertaintyPolicy.FAIL);
+        SemanticQuestion score = new SemanticQuestion(
+                SemanticQuestion.Type.SCORE, "Score", null,
+                Map.of(), IntStream.range(0, 11).mapToObj(i -> "Level " + 
i).toList(), 0.5, 0,
+                SemanticQuestion.UncertaintyPolicy.FAIL);
+        assertThatThrownBy(() -> 
adapter.validate(choice)).isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("255 choice criteria");
+        assertThatThrownBy(() -> adapter.evaluate(score, 
"original")).isInstanceOf(IllegalArgumentException.class)
+                .hasMessageContaining("10 score levels");
+        assertThat(requests).isEmpty();
+    }
+
     private Expression expression(SemanticQuestion.Type type) {
         SemanticQuestion question = new SemanticQuestion(
                 type, "Classify", null,
diff --git 
a/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/ChoiceSelectorJaxbTest.java
 
b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/ChoiceSelectorJaxbTest.java
new file mode 100644
index 000000000000..2a3d591b06d7
--- /dev/null
+++ 
b/components/camel-spring-parent/camel-spring-xml/src/test/java/org/apache/camel/spring/ChoiceSelectorJaxbTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.spring;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.model.ChoiceDefinition;
+import org.apache.camel.model.RoutesDefinition;
+import org.apache.camel.model.language.XPathExpression;
+import org.apache.camel.xml.jaxb.JaxbHelper;
+import org.apache.camel.xml.jaxb.JaxbModelToXMLDumper;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+class ChoiceSelectorJaxbTest {
+    @Test
+    void selectorNamespacesAndLiteralBranchesSurviveJaxbRoundTrip() throws 
Exception {
+        try (var context = new DefaultCamelContext()) {
+            XPathExpression selector = new 
XPathExpression("/t:ticket/t:department/text()");
+            selector.setNamespaces(Map.of("t", "urn:tickets"));
+            RoutesDefinition routes = new RoutesDefinition();
+            routes.from("direct:start").routeId("selector").choice(selector)
+                    
.when("billing").to("mock:billing").otherwise().to("mock:other");
+            String xml = new JaxbModelToXMLDumper().dumpModelAsXml(context, 
routes);
+            try (var input = new 
ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))) {
+                RoutesDefinition restored = 
JaxbHelper.loadRoutesDefinition(context, input);
+                ChoiceDefinition choice = (ChoiceDefinition) 
restored.getRoutes().get(0).getOutputs().get(0);
+                XPathExpression xpath = (XPathExpression) 
choice.getSelector().getExpressionType();
+                assertEquals("/t:ticket/t:department/text()", 
xpath.getExpression());
+                assertEquals("urn:tickets", xpath.getNamespaces().get("t"));
+                assertEquals("billing", 
choice.getWhenClauses().get(0).getValue());
+                assertNull(choice.getWhenClauses().get(0).getExpression());
+            }
+        }
+    }
+}
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/processor/ChoiceSelectorTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/processor/ChoiceSelectorTest.java
index 3e567356fd57..9ac8ab89a238 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/processor/ChoiceSelectorTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/processor/ChoiceSelectorTest.java
@@ -140,6 +140,24 @@ class ChoiceSelectorTest extends ContextTestSupport {
         invalid("without predicates", choice -> choice.when(exchange -> 
true).to("mock:mixed"));
     }
 
+    @Test
+    void emptyLiteralMatchesAndUnmatchedWithoutOtherwiseContinues() throws 
Exception {
+        context.addRoutes(new RouteBuilder() {
+            public void configure() {
+                
from("direct:start").choice(selector()).when("").to("mock:empty")
+                    .end().to("mock:after");
+            }
+        });
+        context.start();
+        getMockEndpoint("mock:empty").expectedBodiesReceived("empty");
+        getMockEndpoint("mock:after").expectedBodiesReceived("empty", 
"unmatched", "null");
+        template.sendBodyAndHeader("direct:start", "empty", "department", "");
+        template.sendBodyAndHeader("direct:start", "unmatched", "department", 
"other");
+        template.sendBody("direct:start", "null");
+        assertMockEndpointsSatisfied();
+        assertEquals(3, calls.get());
+    }
+
     private void invalid(String message, Consumer<ChoiceDefinition> configure) 
throws Exception {
         context.addRoutes(new RouteBuilder() {
             public void configure() {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
index 247d49a96e00..8b7085f8f401 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceEditAssist.java
@@ -17,6 +17,7 @@
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
 import java.io.IOException;
+import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
@@ -1028,7 +1029,37 @@ final class SourceEditAssist {
         if (nodes == null) {
             return null;
         }
-        return (JsonObject) nodes.get(nodeName);
+        if (!nodeName.startsWith("/")) {
+            return (JsonObject) nodes.get(nodeName);
+        }
+        JsonObject node = (JsonObject) nodes.get("root");
+        String[] path = nodeName.substring(1).split("/");
+        for (String part : path) {
+            if (node != null && "map".equals(node.get("type"))) {
+                node = (JsonObject) nodes.get(node.get("ref"));
+                continue;
+            }
+            JsonObject child = null;
+            if (node != null && node.get("children") instanceof JsonArray 
children) {
+                String key = URLDecoder.decode(part, StandardCharsets.UTF_8);
+                for (Object item : children) {
+                    JsonObject candidate = (JsonObject) item;
+                    if (key.equals(candidate.get("name"))) {
+                        child = candidate;
+                        break;
+                    }
+                }
+            }
+            node = null;
+            if (child != null) {
+                node = "map".equals(child.get("type")) ? child : (JsonObject) 
nodes.get(child.get("ref"));
+            }
+            if (node == null) {
+                // Keep the existing lookup for positions whose ancestors are 
not represented by the tree.
+                return (JsonObject) 
nodes.get(URLDecoder.decode(path[path.length - 1], StandardCharsets.UTF_8));
+            }
+        }
+        return node;
     }
 
     List<AutocompletePopup.CompletionItem> provideTreeCompletions(String 
contextAfterPrefix) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index b489a9666b04..61427ee054d2 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -1107,6 +1107,7 @@ class SourceViewer {
         // tree-driven completion — walk up to find parent key, use completion 
tree
         if (autocompleteProvider != null) {
             String parentKey = yaml.findParentYamlKey(row);
+            String parentPath = yaml.findParentYamlPath(row);
             int colonIdx = trimmed.indexOf(':');
 
             if (colonIdx > 0) {
@@ -1120,7 +1121,7 @@ class SourceViewer {
                     valueText = valueText.substring(0, valueText.length() - 1);
                 }
                 if (autocompleteValueProvider != null) {
-                    String context = "yaml-tree-value:" + parentKey + ":" + 
optionName;
+                    String context = "yaml-tree-value:" + parentPath + ":" + 
optionName;
                     List<AutocompletePopup.CompletionItem> values = 
autocompleteValueProvider.provide(context);
                     if (values != null && !values.isEmpty()) {
                         autocompletePopup = new AutocompletePopup(values, "", 
valueText, true);
@@ -1130,7 +1131,7 @@ class SourceViewer {
                 // key completion
                 String filter = trimmed;
                 java.util.Set<String> existing = 
yaml.collectExistingSiblingKeys(row);
-                String context = "yaml-tree:" + parentKey;
+                String context = "yaml-tree:" + parentPath;
                 if (!existing.isEmpty()) {
                     context += ":" + String.join(",", existing);
                 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlSourceContext.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlSourceContext.java
index 7d31de1840c6..148c915868c5 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlSourceContext.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/YamlSourceContext.java
@@ -16,6 +16,8 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.tui;
 
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashSet;
@@ -481,6 +483,26 @@ final class YamlSourceContext {
         return "root";
     }
 
+    String findParentYamlPath(int fromRow) {
+        String line = editState.getLine(fromRow);
+        int indent = line.isBlank() ? effectiveBlankIndent(fromRow) : 
countLeadingSpaces(line);
+        List<String> keys = new ArrayList<>();
+        for (int i = fromRow - 1; i >= 0; i--) {
+            String parent = editState.getLine(i);
+            int parentIndent = countLeadingSpaces(parent);
+            if (!parent.isBlank() && parentIndent < indent) {
+                String key = extractEipName(parent.trim());
+                if (key == null) {
+                    break;
+                }
+                keys.add(URLEncoder.encode(dashToCamelCase(key), 
StandardCharsets.UTF_8));
+                indent = parentIndent;
+            }
+        }
+        Collections.reverse(keys);
+        return keys.isEmpty() ? "root" : "/" + String.join("/", keys);
+    }
+
     YamlEipContext findEnclosingEip(int fromRow) {
         String cursorLine = editState.getLine(fromRow);
         int cursorIndent = cursorLine.isBlank() ? 
effectiveBlankIndent(fromRow) : countLeadingSpaces(cursorLine);
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticCompletionTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticCompletionTest.java
new file mode 100644
index 000000000000..98934f8df4e4
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SemanticCompletionTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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.tui;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+import dev.tamboui.widgets.input.TextAreaState;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class SemanticCompletionTest {
+    private SourceEditAssist assist() {
+        return new SourceEditAssist(
+                new MonitorContext(
+                        new AtomicReference<List<IntegrationInfo>>(List.of()),
+                        new AtomicReference<List<InfraInfo>>(List.of())));
+    }
+
+    @Test
+    void completionFollowsNamedQuestionMapAndOffersVariantFields() {
+        TextAreaState state = new TextAreaState("- semantic:\n    question:\n  
    department:\n        ");
+        SourceEditorNavigation.positionCursor(state, 3, 8);
+        String path = new YamlSourceContext(state).findParentYamlPath(3);
+        assertEquals("/semantic/question/department", path);
+        SourceEditAssist assist = assist();
+        List<String> keys = 
assist.provideTreeCompletions(path).stream().map(AutocompletePopup.CompletionItem::key).toList();
+        assertTrue(keys.containsAll(List.of("type", "instructions", "state", 
"criteria", "threshold", "uncertaintyPolicy")),
+                keys.toString());
+        assertFalse(assist.provideTreeCompletions(path + 
":instructions").stream()
+                .anyMatch(item -> item.key().equals("instructions")));
+        List<String> types = assist.provideTreeValueCompletions(path + 
":type").stream()
+                .map(AutocompletePopup.CompletionItem::key).toList();
+        assertTrue(types.containsAll(List.of("boolean", "choice", "score")), 
types.toString());
+        
assertFalse(String.valueOf(assist.getTreeNode("semantic").get("label")).contains("language"));
+    }
+
+    @Test
+    void ordinaryRouteCompletionStillResolvesThroughItsAncestors() {
+        SourceEditAssist assist = assist();
+        assertEquals(assist.provideTreeCompletions("log"), 
assist.provideTreeCompletions("/route/from/steps/log"));
+    }
+}
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/SemanticDefinitionDeserializer.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/SemanticDefinitionDeserializer.java
index 7c760c8a44e7..64d93f41b03b 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/SemanticDefinitionDeserializer.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/SemanticDefinitionDeserializer.java
@@ -71,8 +71,7 @@ public class SemanticDefinitionDeserializer extends 
YamlDeserializerSupport impl
                 }
             }
         }
-        String location = dc.getResource().getLocation();
-        SemanticQuestions.get(context).replace(location, definitions);
+        SemanticQuestions.get(context).replace(dc.getResource(), definitions);
     }
 
     private static Map<String, SemanticQuestion> read(Node node) {
@@ -83,11 +82,6 @@ public class SemanticDefinitionDeserializer extends 
YamlDeserializerSupport impl
         Map<String, SemanticQuestion> result = new LinkedHashMap<>();
         fields(semantic.get("question")).forEach((name, definition) -> {
             Map<String, Node> values = fields(definition);
-            if (values.containsKey("uncertainty-policy")) {
-                if (values.putIfAbsent("uncertaintyPolicy", 
values.remove("uncertainty-policy")) != null) {
-                    throw new IllegalArgumentException("Duplicate uncertainty 
policy: " + name);
-                }
-            }
             if (!FIELDS.containsAll(values.keySet())) {
                 throw new IllegalArgumentException("Unknown property in 
semantic question: " + name);
             }
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-maven-plugin/src/main/java/org/apache/camel/maven/dsl/yaml/GenerateYamlCompletionMojo.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-maven-plugin/src/main/java/org/apache/camel/maven/dsl/yaml/GenerateYamlCompletionMojo.java
index 157f0badaf1a..3d6a03832da5 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-maven-plugin/src/main/java/org/apache/camel/maven/dsl/yaml/GenerateYamlCompletionMojo.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-maven-plugin/src/main/java/org/apache/camel/maven/dsl/yaml/GenerateYamlCompletionMojo.java
@@ -376,6 +376,14 @@ public class GenerateYamlCompletionMojo extends 
AbstractMojo {
             }
 
             ArrayNode children = buildChildrenFromDefinition(def, fqName);
+            for (JsonNode group : def.path("anyOf")) {
+                for (JsonNode variant : group.path("oneOf")) {
+                    String variantName = resolveRefFq(variant);
+                    if (variantName != null && definitions.has(variantName)) {
+                        mergeChildren(children, 
buildChildrenFromDefinition(definitions.get(variantName), variantName));
+                    }
+                }
+            }
             node.set("children", children);
 
             nodes.set(nodeName, node);
@@ -418,7 +426,8 @@ public class GenerateYamlCompletionMojo extends 
AbstractMojo {
         }
 
         // check for language model
-        LanguageModel langModel = catalog.languageModel(nodeName);
+        LanguageModel langModel = 
fqName.startsWith("org.apache.camel.model.language.")
+                ? catalog.languageModel(nodeName) : null;
         if (langModel != null) {
             node.put("title", langModel.getTitle());
             node.put("description", langModel.getDescription());
@@ -448,7 +457,7 @@ public class GenerateYamlCompletionMojo extends 
AbstractMojo {
 
         // look up catalog model for enriched option metadata
         String nodeName = defToNodeName.get(fqName);
-        Map<String, BaseOptionModel> catalogOptions = 
loadCatalogOptions(nodeName);
+        Map<String, BaseOptionModel> catalogOptions = 
loadCatalogOptions(nodeName, fqName);
 
         Iterator<Map.Entry<String, JsonNode>> propFields = props.fields();
         while (propFields.hasNext()) {
@@ -480,6 +489,13 @@ public class GenerateYamlCompletionMojo extends 
AbstractMojo {
                     child.put("type", type);
                 }
             }
+            if (prop.has("additionalProperties") && 
prop.get("additionalProperties").isObject()) {
+                String entryRef = resolveRef(prop.get("additionalProperties"));
+                if (entryRef != null) {
+                    child.put("type", "map");
+                    child.put("ref", entryRef);
+                }
+            }
 
             putIfPresent(child, "description", prop, "description");
             putIfPresent(child, "title", prop, "title");
@@ -546,7 +562,35 @@ public class GenerateYamlCompletionMojo extends 
AbstractMojo {
         return children;
     }
 
-    private Map<String, BaseOptionModel> loadCatalogOptions(String nodeName) {
+    private void mergeChildren(ArrayNode children, ArrayNode variants) {
+        for (JsonNode variant : variants) {
+            ObjectNode existing = null;
+            for (JsonNode child : children) {
+                if (child.path("name").equals(variant.path("name"))) {
+                    existing = (ObjectNode) child;
+                    break;
+                }
+            }
+            if (existing == null) {
+                children.add(variant);
+            } else if (existing.has("enum") && variant.has("enum")) {
+                ArrayNode values = (ArrayNode) existing.get("enum");
+                variant.get("enum").forEach(value -> {
+                    boolean present = false;
+                    for (JsonNode item : values) {
+                        present |= item.equals(value);
+                    }
+                    if (!present) {
+                        values.add(value);
+                    }
+                });
+            } else if (!existing.path("type").equals(variant.path("type"))) {
+                existing.put("type", "any");
+            }
+        }
+    }
+
+    private Map<String, BaseOptionModel> loadCatalogOptions(String nodeName, 
String fqName) {
         Map<String, BaseOptionModel> optMap = new LinkedHashMap<>();
         if (nodeName == null) {
             return optMap;
@@ -562,7 +606,8 @@ public class GenerateYamlCompletionMojo extends 
AbstractMojo {
         }
 
         // try language model
-        LanguageModel langModel = catalog.languageModel(nodeName);
+        LanguageModel langModel = 
fqName.startsWith("org.apache.camel.model.language.")
+                ? catalog.languageModel(nodeName) : null;
         if (langModel != null) {
             for (LanguageModel.LanguageOptionModel opt : 
langModel.getOptions()) {
                 optMap.put(opt.getName(), opt);
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
index 6c91cf6bb8f4..97a401de8433 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/SemanticSchemaTest.java
@@ -39,9 +39,14 @@ class SemanticSchemaTest {
         assertThat(validator.validate(choice)).isEmpty();
         assertThat(validator.validate(choice.replace("type: choice", "type: 
boolean")))
                 .isEmpty(); // criterion key constraints are checked by the 
runtime
-        assertThat(validator.validate(choice.replace("type: choice", "type: 
score"))).isNotEmpty();
-        assertThat(validator.validate(choice.replace("instructions:", 
"threshold: 0.5\n        instructions:"))).isNotEmpty();
-        assertThat(validator.validate(choice.replace("instructions:", 
"typo:"))).isNotEmpty();
+        for (String invalid : new String[] {
+                choice.replace("type: choice", "type: score"),
+                choice.replace("instructions:", "threshold: 0.5\n        
instructions:"),
+                choice.replace("instructions:", "typo:"),
+                choice.replace("instructions:", "uncertainty-policy: fail\n    
    instructions:") }) {
+            assertThat(validator.validate(invalid)).isNotEmpty().allSatisfy(
+                    error -> 
assertThat(error.getInstanceLocation().toString()).startsWith("/0/semantic/question/topic"));
+        }
         assertThat(validator.validate("""
                 - semantic:
                     question:
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml
index f064d0132cf5..d65c92e7c259 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml
@@ -44,6 +44,11 @@
     </properties>
 
     <dependencies>
+        <dependency>
+            <groupId>org.apache.camel</groupId>
+            <artifactId>camel-yaml-io</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.apache.camel</groupId>
             <artifactId>camel-yaml-dsl-common</artifactId>
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
index 9a2023971bc9..7f813a5f8ecf 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
@@ -863,13 +863,11 @@
       } ]
     },
     "semantic" : {
-      "title" : "Semantic",
-      "description" : "Evaluate named semantic questions through a provider 
adapter",
-      "label" : "language,ai",
       "isListItem" : true,
       "children" : [ {
         "name" : "question",
-        "type" : "object",
+        "type" : "map",
+        "ref" : "semanticDefinitionDeserializer$QuestionSchema",
         "required" : true
       } ]
     },
@@ -921,7 +919,32 @@
       } ]
     },
     "semanticDefinitionDeserializer$QuestionSchema" : {
-      "children" : [ ]
+      "children" : [ {
+        "name" : "criteria",
+        "type" : "any"
+      }, {
+        "name" : "instructions",
+        "type" : "string",
+        "required" : true
+      }, {
+        "name" : "state",
+        "type" : "string"
+      }, {
+        "name" : "threshold",
+        "type" : "number"
+      }, {
+        "name" : "type",
+        "type" : "enum",
+        "enum" : [ "boolean", "choice", "score" ],
+        "required" : true
+      }, {
+        "name" : "uncertainty",
+        "type" : "number"
+      }, {
+        "name" : "uncertaintyPolicy",
+        "type" : "enum",
+        "enum" : [ "fail", "non-match" ]
+      } ]
     },
     "semanticDefinitionDeserializer$ScoreSchema" : {
       "children" : [ {
@@ -1374,9 +1397,9 @@
       } ]
     },
     "bean" : {
-      "title" : "Bean Method",
-      "description" : "Calls a Java bean method",
-      "label" : "language,core,java",
+      "title" : "Bean",
+      "description" : "Invokes a method on a Java bean, with automatic 
parameter binding from the message body, headers, and exchange properties",
+      "label" : "eip,endpoint",
       "input" : true,
       "isListItem" : true,
       "children" : [ {
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java
index ffbfe8cd4ecc..73c323456942 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/main/java/org/apache/camel/dsl/yaml/YamlRoutesBuilderLoader.java
@@ -358,7 +358,9 @@ public class YamlRoutesBuilderLoader extends 
YamlRoutesBuilderLoaderSupport {
     }
 
     private Object preConfigureNode(Node root, YamlDeserializationContext ctx, 
boolean preParse) {
-        SemanticDefinitionDeserializer.configure(getCamelContext(), ctx, root);
+        if (preParse) {
+            SemanticDefinitionDeserializer.configure(getCamelContext(), ctx, 
root);
+        }
         // backwards compatible fixes
         Object target = root;
 
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/ChoiceSelectorYamlDumpTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/ChoiceSelectorYamlDumpTest.java
new file mode 100644
index 000000000000..5404aa2320c6
--- /dev/null
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/ChoiceSelectorYamlDumpTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.yaml;
+
+import java.util.Map;
+
+import org.apache.camel.dsl.yaml.support.YamlTestSupport;
+import org.apache.camel.model.ChoiceDefinition;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.language.XPathExpression;
+import org.apache.camel.yaml.LwModelToYAMLDumper;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class ChoiceSelectorYamlDumpTest extends YamlTestSupport {
+    @Test
+    void selectorNamespacesAndLiteralBranchesSurviveYamlDump() throws 
Exception {
+        XPathExpression selector = new 
XPathExpression("/t:ticket/t:department/text()");
+        selector.setNamespaces(Map.of("t", "urn:tickets"));
+        RouteDefinition route = new 
RouteDefinition().from("direct:start").routeId("selector");
+        
route.choice(selector).when("billing").to("mock:billing").otherwise().to("mock:other");
+        String yaml = new LwModelToYAMLDumper().dumpModelAsYaml(context, 
route);
+        loadRoutes(yaml);
+        ChoiceDefinition restored = (ChoiceDefinition) 
context.getRouteDefinition("selector").getOutputs().get(0);
+        XPathExpression xpath = (XPathExpression) 
restored.getSelector().getExpressionType();
+        
assertThat(xpath.getExpression()).isEqualTo("/t:ticket/t:department/text()");
+        assertThat(xpath.getNamespaces()).containsEntry("t", "urn:tickets");
+        
assertThat(restored.getWhenClauses().get(0).getValue()).isEqualTo("billing");
+        assertThat(restored.getWhenClauses().get(0).getExpression()).isNull();
+    }
+}
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java
index e8c4c2c46c2e..4b71a86826ae 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/SemanticQuestionTest.java
@@ -16,6 +16,9 @@
  */
 package org.apache.camel.dsl.yaml;
 
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.camel.component.mock.MockEndpoint;
@@ -25,14 +28,19 @@ import org.apache.camel.semantic.SemanticAdapter;
 import org.apache.camel.semantic.SemanticQuestion;
 import org.apache.camel.semantic.SemanticQuestions;
 import org.apache.camel.semantic.SemanticResult;
+import org.apache.camel.spi.Resource;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.support.RouteWatcherReloadStrategy;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 class SemanticQuestionTest extends YamlTestSupport {
+    @TempDir
+    Path directory;
     private final AtomicInteger calls = new AtomicInteger();
     private Object selected;
 
@@ -136,14 +144,46 @@ class SemanticQuestionTest extends YamlTestSupport {
         assertThatThrownBy(() -> 
SemanticQuestions.get(context).get("department")).hasMessageContaining("Unknown");
     }
 
+    @Test
+    void watcherDropsDeletedQuestionOnlyResourcesBeforeLoadingRenamedFiles() 
throws Exception {
+        Path original = directory.resolve("questions.yaml");
+        Files.writeString(original, declarations("${body}"));
+        Resource source = ResourceHelper.resolveResource(context, 
original.toUri().toString());
+        loadRoutes(source);
+        context.start();
+        TestWatcher watcher = new TestWatcher();
+        watcher.setCamelContext(context);
+        Path renamed = Files.move(original, directory.resolve("q.yaml"));
+        watcher.reload(source);
+        assertThatThrownBy(() -> 
SemanticQuestions.get(context).get("department"))
+                
.isInstanceOf(IllegalArgumentException.class).hasMessageContaining("Unknown");
+        Resource replacement = ResourceHelper.resolveResource(context, 
renamed.toUri().toString());
+        watcher.reload(replacement);
+        assertThat(watcher.getLastError()).isNull();
+        
assertThat(SemanticQuestions.get(context).get("department").getState()).isEqualTo("${body}");
+        Files.delete(renamed);
+        watcher.reload(replacement);
+        assertThatThrownBy(() -> 
SemanticQuestions.get(context).get("department"))
+                
.isInstanceOf(IllegalArgumentException.class).hasMessageContaining("Unknown");
+    }
+
+    private static class TestWatcher extends RouteWatcherReloadStrategy {
+        void reload(Resource resource) {
+            onRouteReload(List.of(resource), false);
+        }
+    }
+
     @Test
     void duplicatesAndInvalidDefinitionsAreRejected() {
         assertThatThrownBy(() -> loadRoutesNoValidate(declarations("${body}") 
+ declarations("${body}")))
                 .hasStackTraceContaining("Duplicate semantic question");
         assertThatThrownBy(() -> 
loadRoutesNoValidate(declarations("${body}").replace("instructions:", "typo:")))
                 .hasStackTraceContaining("Unknown property");
-        assertThatThrownBy(() -> 
loadRoutes(declarations("${body}").replace("type: choice", "type: 
unsupported")))
-                .isInstanceOf(Exception.class);
+        assertThatThrownBy(() -> 
loadRoutesNoValidate(declarations("${body}").replace("type: choice", "type: 
unsupported")))
+                
.hasRootCauseInstanceOf(IllegalArgumentException.class).hasStackTraceContaining("UNSUPPORTED");
+        assertThatThrownBy(() -> loadRoutesNoValidate(declarations("${body}")
+                .replace("instructions:", "uncertainty-policy: fail\n        
instructions:")))
+                .hasStackTraceContaining("Unknown property");
     }
 
     @Test
@@ -163,7 +203,7 @@ class SemanticQuestionTest extends YamlTestSupport {
                         criteria: [Routine, Urgent]
                 """);
         
assertThat(SemanticQuestions.get(context).get("urgency").getLevels()).containsExactly("Routine",
 "Urgent");
-        assertThatThrownBy(() -> 
loadRoutes(declarations("${body}").replace("type: choice", "type: score")))
-                .isInstanceOf(Exception.class);
+        assertThatThrownBy(() -> 
loadRoutesNoValidate(declarations("${body}").replace("type: choice", "type: 
score")))
+                .hasStackTraceContaining("Node type map is invalid, expected 
array");
     }
 }

Reply via email to