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 0601aa9abca9d6fb6d3df173010e655f0a12137c
Author: Luigi De Masi <[email protected]>
AuthorDate: Wed Sep 30 08:47:53 2026 +0200

    CAMEL-25138: Refine semantic declaration export
    
    Export registered questions through the model configurer SPI, including
    lazy discovery for YAML-only declarations, and omit default boolean
    decision policies from converted output.
    
    Cover Java filter and nested Simple expression conversion to YAML/XML,
    default-policy round trips, and numeric placeholders. Include the
    underlying error when conversion fails and document temporary-context
    property resolution.
    
    Validation: 549 tests passed on Java 17 across core-model, semantic and
    MCP. Full repository clean install with tests skipped passed on Java 21
    across all 695 modules. Regenerated catalog documentation is included.
    
    Co-authored-by: Codex <[email protected]>
    Signed-off-by: Luigi De Masi <[email protected]>
---
 .../camel/catalog/docs/semantic-language.adoc      | 11 ++-
 .../src/main/docs/semantic-language.adoc           | 11 ++-
 .../DefaultSemanticDefinitionConfigurer.java       | 15 +++-
 .../camel/model/SemanticDefinitionHelper.java      | 29 +++++--
 .../model/spi/SemanticDefinitionConfigurer.java    |  5 ++
 .../jbang/core/commands/mcp/TransformTools.java    | 16 ++--
 .../core/commands/mcp/TransformToolsTest.java      | 95 +++++++++++++++++++++-
 7 files changed, 160 insertions(+), 22 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 06e37072ac63..c1a09c185936 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
@@ -184,8 +184,15 @@ can also be selected together using `refs:name1,name2`, as 
described below.
 
 Direct exports of a `RoutesDefinition` containing semantic declarations 
preserve them. The MCP
 route transformation tool also preserves declarations in XML-to-YAML, 
YAML-to-XML, and
-Java-to-YAML/XML conversions. Conversions that load a Camel context export the 
registered
-questions with their effective numeric policies. Runtime route dumps such as 
`camel.main.dumpRoutes=yaml` contain only
+Java-to-YAML/XML conversions. YAML-to-XML and Java-to-YAML/XML conversions 
load a temporary
+Camel context and export the registered questions with their effective numeric 
policies,
+omitting settings that match the defaults. Numeric placeholders must resolve 
in that context;
+the tool does not load the application's external properties. Provide a 
placeholder default,
+such as `{{semantic.threshold:0.5}}`, or a concrete value when the property is 
unavailable.
+Unresolved required properties fail conversion. XML-to-YAML conversion 
preserves placeholder
+text without resolving it.
+
+Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only
 the routes and omit the question declarations. Supply the declarations 
separately before
 reloading those dumps.
 
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 06e37072ac63..c1a09c185936 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
@@ -184,8 +184,15 @@ can also be selected together using `refs:name1,name2`, as 
described below.
 
 Direct exports of a `RoutesDefinition` containing semantic declarations 
preserve them. The MCP
 route transformation tool also preserves declarations in XML-to-YAML, 
YAML-to-XML, and
-Java-to-YAML/XML conversions. Conversions that load a Camel context export the 
registered
-questions with their effective numeric policies. Runtime route dumps such as 
`camel.main.dumpRoutes=yaml` contain only
+Java-to-YAML/XML conversions. YAML-to-XML and Java-to-YAML/XML conversions 
load a temporary
+Camel context and export the registered questions with their effective numeric 
policies,
+omitting settings that match the defaults. Numeric placeholders must resolve 
in that context;
+the tool does not load the application's external properties. Provide a 
placeholder default,
+such as `{{semantic.threshold:0.5}}`, or a concrete value when the property is 
unavailable.
+Unresolved required properties fail conversion. XML-to-YAML conversion 
preserves placeholder
+text without resolving it.
+
+Runtime route dumps such as `camel.main.dumpRoutes=yaml` contain only
 the routes and omit the question declarations. Supply the declarations 
separately before
 reloading those dumps.
 
diff --git 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java
 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java
index 9971fe18596d..909454e17faf 100644
--- 
a/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java
+++ 
b/components/camel-ai/camel-semantic/src/main/java/org/apache/camel/semantic/DefaultSemanticDefinitionConfigurer.java
@@ -53,8 +53,8 @@ public class DefaultSemanticDefinitionConfigurer implements 
SemanticDefinitionCo
         SemanticQuestions.get(context).replace("model:" + source, resource, 
questions);
     }
 
-    /** Export a snapshot of the registered questions and their effective 
decision policies. */
-    public static SemanticDefinition getDefinition(CamelContext context) {
+    @Override
+    public SemanticDefinition getDefinition(CamelContext context) {
         SemanticQuestions questions = 
context.getCamelContextExtension().getContextPlugin(SemanticQuestions.class);
         if (questions == null) {
             return null;
@@ -68,8 +68,15 @@ public class DefaultSemanticDefinitionConfigurer implements 
SemanticDefinitionCo
             question.getCriteria().forEach(target::criterion);
             question.getLevels().forEach(target::level);
             if (question.getType() == SemanticQuestion.Type.BOOLEAN) {
-                
target.threshold(question.getThreshold()).uncertainty(question.getUncertainty())
-                        
.uncertaintyPolicy(question.getUncertaintyPolicy().name().toLowerCase(Locale.ROOT).replace('_',
 '-'));
+                if (question.getThreshold() != 0.5) {
+                    target.threshold(question.getThreshold());
+                }
+                if (question.getUncertainty() != 0) {
+                    target.uncertainty(question.getUncertainty());
+                }
+                if (question.getUncertaintyPolicy() != 
SemanticQuestion.UncertaintyPolicy.FAIL) {
+                    
target.uncertaintyPolicy(question.getUncertaintyPolicy().name().toLowerCase(Locale.ROOT).replace('_',
 '-'));
+                }
             }
         });
         return definition;
diff --git 
a/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java
 
b/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java
index 95f4a8b1c922..57ab5253c1e2 100644
--- 
a/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java
+++ 
b/core/camel-core-model/src/main/java/org/apache/camel/model/SemanticDefinitionHelper.java
@@ -35,14 +35,33 @@ public final class SemanticDefinitionHelper {
             if (definition == null || definition.getQuestions().isEmpty()) {
                 return;
             }
+            configurer = getConfigurer(context);
+            if (configurer == null) {
+                throw new IllegalArgumentException("Semantic question 
declarations require camel-semantic on the classpath");
+            }
+        }
+        configurer.configure(context, resource, source, definition);
+    }
+
+    /** Export registered questions without requiring the semantic module for 
ordinary routes. */
+    public static SemanticDefinition getDefinition(CamelContext context) {
+        // YAML declarations can populate the registry before the configurer 
has been discovered.
+        SemanticDefinitionConfigurer configurer = getConfigurer(context);
+        return configurer != null ? configurer.getDefinition(context) : null;
+    }
+
+    private static SemanticDefinitionConfigurer getConfigurer(CamelContext 
context) {
+        SemanticDefinitionConfigurer configurer
+                = 
context.getCamelContextExtension().getContextPlugin(SemanticDefinitionConfigurer.class);
+        if (configurer == null) {
             // Concurrent discovery may create equivalent instances of the 
stateless default configurer.
             // The semantic module synchronizes access to shared question 
state.
             configurer = 
context.getCamelContextExtension().getDefaultFactoryFinder()
-                    .newInstance("semantic-configurer", 
SemanticDefinitionConfigurer.class)
-                    .orElseThrow(() -> new IllegalArgumentException(
-                            "Semantic question declarations require 
camel-semantic on the classpath"));
-            
context.getCamelContextExtension().addContextPlugin(SemanticDefinitionConfigurer.class,
 configurer);
+                    .newInstance("semantic-configurer", 
SemanticDefinitionConfigurer.class).orElse(null);
+            if (configurer != null) {
+                
context.getCamelContextExtension().addContextPlugin(SemanticDefinitionConfigurer.class,
 configurer);
+            }
         }
-        configurer.configure(context, resource, source, definition);
+        return configurer;
     }
 }
diff --git 
a/core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java
 
b/core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java
index 06485ca98106..38db8263f6b5 100644
--- 
a/core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java
+++ 
b/core/camel-core-model/src/main/java/org/apache/camel/model/spi/SemanticDefinitionConfigurer.java
@@ -27,4 +27,9 @@ public interface SemanticDefinitionConfigurer {
      * the owner independently of the optional resource used to track deletion.
      */
     void configure(CamelContext context, Resource resource, String source, 
SemanticDefinition definition);
+
+    /** Export registered questions with their effective policies, or {@code 
null} if no definitions are available. */
+    default SemanticDefinition getDefinition(CamelContext context) {
+        return null;
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
index 92cb87bb0fd2..db658a0f65a3 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformTools.java
@@ -34,11 +34,12 @@ import org.apache.camel.model.ExpressionNode;
 import org.apache.camel.model.ProcessorDefinitionHelper;
 import org.apache.camel.model.RouteDefinition;
 import org.apache.camel.model.RoutesDefinition;
+import org.apache.camel.model.SemanticDefinitionHelper;
 import org.apache.camel.model.app.SemanticDefinition;
-import org.apache.camel.semantic.DefaultSemanticDefinitionConfigurer;
 import org.apache.camel.spi.Resource;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.util.ObjectHelper;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.xml.in.ModelParser;
 import org.apache.camel.yaml.out.YamlModelWriter;
@@ -100,8 +101,13 @@ public class TransformTools {
                 result.note = "Unsupported transformation: " + fromFormat + " 
to " + toFormat;
             }
         } catch (Throwable e) {
+            Throwable cause = ObjectHelper.createExceptionIterator(e).next();
+            String message = e.getMessage();
+            if (cause != e) {
+                message += ": " + cause.getMessage();
+            }
             throw new ToolCallException(
-                    "Failed to transform route (" + e.getClass().getName() + 
"): " + e.getMessage(), null);
+                    "Failed to transform route (" + e.getClass().getName() + 
"): " + message, null);
         }
 
         return result;
@@ -158,7 +164,7 @@ public class TransformTools {
 
             RoutesDefinition rd = new RoutesDefinition();
             rd.setRoutes(routeDefs);
-            
rd.setSemantic(DefaultSemanticDefinitionConfigurer.getDefinition(ctx));
+            rd.setSemantic(SemanticDefinitionHelper.getDefinition(ctx));
 
             StringWriter sw = new StringWriter();
             new 
org.apache.camel.xml.out.ModelWriter(sw).writeRoutesDefinition(rd);
@@ -191,7 +197,7 @@ public class TransformTools {
             if ("yaml".equals(targetFormat)) {
                 YamlModelWriter writer = new YamlModelWriter();
                 List<JsonObject> roots = new ArrayList<>();
-                SemanticDefinition semantic = 
DefaultSemanticDefinitionConfigurer.getDefinition(ctx);
+                SemanticDefinition semantic = 
SemanticDefinitionHelper.getDefinition(ctx);
                 if (semantic != null) {
                     roots.add(writer.writeSemanticDefinition(semantic));
                 }
@@ -202,7 +208,7 @@ public class TransformTools {
             } else {
                 RoutesDefinition rd = new RoutesDefinition();
                 rd.setRoutes(routeDefs);
-                
rd.setSemantic(DefaultSemanticDefinitionConfigurer.getDefinition(ctx));
+                rd.setSemantic(SemanticDefinitionHelper.getDefinition(ctx));
 
                 StringWriter sw = new StringWriter();
                 new 
org.apache.camel.xml.out.ModelWriter(sw).writeRoutesDefinition(rd);
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java
index 4c45f5b9eb78..36170287d720 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/test/java/org/apache/camel/dsl/jbang/core/commands/mcp/TransformToolsTest.java
@@ -16,15 +16,20 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.mcp;
 
+import io.quarkiverse.mcp.server.ToolCallException;
 import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.model.FilterDefinition;
+import org.apache.camel.model.SetBodyDefinition;
 import org.apache.camel.semantic.SemanticQuestions;
 import org.apache.camel.support.PluginHelper;
 import org.apache.camel.support.ResourceHelper;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.CsvSource;
+import org.junit.jupiter.params.provider.ValueSource;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 class TransformToolsTest {
 
@@ -74,8 +79,8 @@ class TransformToolsTest {
                       urgent:
                         type: boolean
                         instructions: Urgent?
-                        threshold: 0.8
-                        uncertainty: 0.1
+                        threshold: "{{semantic.export.threshold:0.8}}"
+                        uncertainty: "{{semantic.export.uncertainty:0.1}}"
                         uncertaintyPolicy: non-match
                       department:
                         type: choice
@@ -103,7 +108,8 @@ class TransformToolsTest {
                 public class SemanticRoute extends RouteBuilder {
                     public void configure() {
                         
semanticQuestions().question("urgent").type("boolean").instructions("Urgent?")
-                            
.threshold(0.8).uncertainty(0.1).uncertaintyPolicy("non-match");
+                            .threshold("{{semantic.export.threshold:0.8}}")
+                            
.uncertainty("{{semantic.export.uncertainty:0.1}}").uncertaintyPolicy("non-match");
                         
semanticQuestions().question("department").type("choice").instructions("Which 
department?")
                             .state("${header.selected}").criterion("billing", 
"Invoices").criterion("technical", "Bugs");
                         
semanticQuestions().question("priority").type("score").instructions("Priority?")
@@ -114,7 +120,7 @@ class TransformToolsTest {
                 """;
         var result = createTools().camel_transform_route(source.equals("java") 
? java : yaml, source, target);
         assertThat(result.supported).isTrue();
-        assertThat(result.result).contains("ref:department");
+        
assertThat(result.result).contains("ref:department").doesNotContain("{{semantic.export.");
         try (var context = new DefaultCamelContext()) {
             context.build();
             
PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("result."
 + target, result.result));
@@ -129,6 +135,87 @@ class TransformToolsTest {
         }
     }
 
+    @ParameterizedTest
+    @CsvSource({ "yaml,xml", "java,xml", "java,yaml" })
+    void semanticConversionOmitsDefaultPolicies(String source, String target) 
throws Exception {
+        var result = createTools().camel_transform_route(semanticRoute(source, 
"0.5"), source, target);
+
+        assertThat(result.supported).isTrue();
+        assertThat(result.result).contains("urgent", 
"Urgent?").doesNotContain("threshold", "uncertainty");
+        try (var context = new DefaultCamelContext()) {
+            context.build();
+            
PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("result."
 + target, result.result));
+            var question = SemanticQuestions.get(context).get("urgent");
+            assertThat(question.getThreshold()).isEqualTo(0.5);
+            assertThat(question.getUncertainty()).isZero();
+            
assertThat(question.getUncertaintyPolicy().name()).isEqualTo("FAIL");
+        }
+    }
+
+    @ParameterizedTest
+    @CsvSource({ "yaml,xml", "java,xml", "java,yaml" })
+    void semanticConversionReportsMissingNumericProperty(String source, String 
target) {
+        String route = semanticRoute(source, 
"{{semantic.export.missing.threshold}}");
+
+        assertThatThrownBy(() -> createTools().camel_transform_route(route, 
source, target))
+                .isInstanceOf(ToolCallException.class)
+                .hasMessageContaining("Property with key 
[semantic.export.missing.threshold] not found");
+    }
+
+    private static String semanticRoute(String source, String threshold) {
+        if (source.equals("java")) {
+            return """
+                    
semanticQuestions().question("urgent").type("boolean").instructions("Urgent?")
+                        
.threshold("%s").uncertainty(0).uncertaintyPolicy("fail");
+                    from("direct:input").setBody().language("semantic", 
"ref:urgent");
+                    """.formatted(threshold);
+        }
+        return """
+                - semantic:
+                    question:
+                      urgent:
+                        type: boolean
+                        instructions: Urgent?
+                        threshold: "%s"
+                        uncertainty: 0
+                        uncertaintyPolicy: fail
+                - route:
+                    from:
+                      uri: direct:input
+                      steps:
+                        - setBody:
+                            expression:
+                              language:
+                                language: semantic
+                                expression: ref:urgent
+                """.formatted(threshold);
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = { "yaml", "xml" })
+    void nonSemanticExpressionClausesSurviveConversionAndReload(String target) 
throws Exception {
+        String route = """
+                from("direct:input")
+                    .filter().simple("${body} == 'hello'")
+                        .setBody().simple("${body.toUpperCase()}")
+                    .end();
+                """;
+        var result = createTools().camel_transform_route(route, "java", 
target);
+
+        assertThat(result.supported).isTrue();
+        assertThat(result.result).doesNotContain("semantic");
+        try (var context = new DefaultCamelContext()) {
+            context.build();
+            
PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("result."
 + target, result.result));
+            var filter = (FilterDefinition) 
context.getRouteDefinitions().get(0).getOutputs().get(0);
+            
assertThat(filter.getExpression().getLanguage()).isEqualTo("simple");
+            
assertThat(filter.getExpression().getExpression()).isEqualTo("${body} == 
'hello'");
+            var setBody = (SetBodyDefinition) filter.getOutputs().get(0);
+            
assertThat(setBody.getExpression().getLanguage()).isEqualTo("simple");
+            
assertThat(setBody.getExpression().getExpression()).isEqualTo("${body.toUpperCase()}");
+        }
+    }
+
     @Test
     void transformYamlToXml() {
         TransformTools tools = createTools();

Reply via email to