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

davsclaus pushed a commit to branch fix/CAMEL-25403
in repository https://gitbox.apache.org/repos/asf/camel.git

commit d84ec460ae61cc3364097f0de487da391b9ea9d5
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Oct 7 07:22:40 2026 +0200

    CAMEL-25403: camel-jbang - AI tools: note a Kamelet camel: dependency that 
the template does not use
    
    A Kamelet file whose spec.dependencies lists a component the template does
    not use (camel:timer copied from a sample into an action whose template is a
    setBody) now gets a note in the answer of camel_write_file, camel_edit_file
    and camel_validate_source. A note, not an error: the write is not refused.
    Nothing is said when the template may use the component unseen: a bean, a
    class reference, a placeholder as a scheme, or a component that runs on
    another (cron on quartz, rest-openapi on http).
    
    Over the 259 Kamelets of camel-kamelets it notes 6 dependencies, all unused.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |  18 +++
 .../dsl/jbang/core/commands/ai/KameletChecks.java  | 147 +++++++++++++++++++++
 .../jbang/core/commands/ai/KameletChecksTest.java  |  99 ++++++++++++++
 3 files changed, 264 insertions(+)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index 6f65a77f9ba2..dd6fb1a460c0 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -531,6 +531,7 @@ public final class AuthoringTools {
         result.put("file", file);
         result.put("errors", new JsonArray(errors));
         putKameletGuide(result, file, errors);
+        putKameletNotes(result, file, content);
         // the problems whose fix is certain, as edits an agent can apply 
(camel_edit_file find/replace)
         JsonArray fixes = new JsonArray();
         String[] lines = content.split("\n", -1);
@@ -923,6 +924,20 @@ public final class AuthoringTools {
         return message != null ? message.replaceFirst("^Line \\d+: ", "") : "";
     }
 
+    /**
+     * What a Kamelet file does that works but is not right, which does not 
refuse a write: a camel: dependency its
+     * template does not use (CAMEL-25403).
+     */
+    private static void putKameletNotes(JsonObject result, String file, String 
content) {
+        String name = file != null ? file.toLowerCase(Locale.ROOT) : "";
+        if (name.endsWith(".kamelet.yaml") || name.endsWith(".kamelet.yml")) {
+            List<String> notes = KameletChecks.unusedDependencies(content);
+            if (!notes.isEmpty()) {
+                result.put("notes", new JsonArray(notes));
+            }
+        }
+    }
+
     /** Writes a file after validating it, as {@code camel_write_file} does; 
no confirmation is asked here. */
     public static JsonObject writeFile(ToolContext ctx, Path dir, String file, 
String content, boolean validate) {
         Path path = resolveFile(dir, file);
@@ -981,6 +996,9 @@ public final class AuthoringTools {
             // written with problems the file already had: said, so they are 
not taken for fixed
             result.put("existingProblems", new JsonArray(problemsBefore));
         }
+        if (validate) {
+            putKameletNotes(result, file, content);
+        }
         if (watch) {
             JsonObject reload = ReloadOutcome.await(ctx.pid(), processName, 
sinceKey, RELOAD_WAIT_MILLIS);
             result.put("reload", reload);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecks.java
index 0a6f0267545a..ab5d38c3af38 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecks.java
@@ -22,6 +22,7 @@ import java.io.StringReader;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -371,6 +372,152 @@ public final class KameletChecks {
         return errors;
     }
 
+    /** The scheme of an endpoint uri written as a value: timer:orders, 
kamelet:source, https://... */
+    private static final Pattern URI_SCHEME = 
Pattern.compile("^([a-zA-Z][a-zA-Z0-9+.-]*):");
+
+    /**
+     * The camel: dependencies of a Kamelet file that name a component its 
template does not use: no endpoint of the
+     * template has its scheme, and no language or data format of the template 
is in its artifact. Notes, not errors:
+     * the Kamelet works, but an exported project gets the artifact for 
nothing. A model copies them from a sample whose
+     * template starts from a timer, or from the route that uses the Kamelet 
(CAMEL-25403). Nothing is said when the
+     * template could use a component in a way not visible here: a bean, a 
class reference or a placeholder as a scheme.
+     */
+    public static List<String> unusedDependencies(String content) {
+        List<String> notes = new ArrayList<>();
+        Node root;
+        try {
+            root = new Yaml(new SafeConstructor(new 
LoaderOptions())).compose(new StringReader(content));
+        } catch (Exception e) {
+            return notes;
+        }
+        if (!(root instanceof MappingNode doc) || 
!"Kamelet".equals(scalar(value(doc, "kind")))
+                || !(value(doc, "spec") instanceof MappingNode spec)
+                || !(value(spec, "dependencies") instanceof SequenceNode deps)
+                || !(value(spec, "template") instanceof MappingNode template)) 
{
+            return notes;
+        }
+        Set<String> used = new HashSet<>();
+        if (!collectUsed(null, template, used)) {
+            return notes;
+        }
+        CamelCatalog catalog = catalog();
+        Set<String> usedArtifacts = new HashSet<>();
+        for (String name : used) {
+            String artifact = artifactOf(catalog, name);
+            if (artifact == null) {
+                // https: is a scheme of the http component, not a component 
of its own
+                artifact = alternativeSchemes(catalog).get(name);
+            }
+            if (artifact != null) {
+                usedArtifacts.add(artifact);
+            }
+        }
+        for (Node d : deps.getValue()) {
+            String dep = scalar(d);
+            if (dep == null || !dep.startsWith("camel:")) {
+                continue;
+            }
+            String name = dep.substring("camel:".length());
+            ComponentModel model = catalog.componentModel(name);
+            // only a component is checked: a language or a data format may be 
used in ways the template does not show
+            if (model == null || catalog.languageModel(name) != null || 
catalog.dataFormatModel(name) != null
+                    || usedArtifacts.contains(model.getArtifactId()) || 
delegatedTo(used, name)) {
+                continue;
+            }
+            notes.add(EndpointChecks.linePrefix(line(d)) + "spec.dependencies: 
" + dep + " is not used by the template"
+                      + " (it has no " + name + ": endpoint): list only the 
components, languages and data formats the"
+                      + " template uses");
+        }
+        return notes;
+    }
+
+    /** Components that run on another one the template does not name: cron on 
quartz, rest-openapi on an http one. */
+    private static final Map<String, Pattern> DELEGATES = Map.of(
+            "cron", Pattern.compile("quartz|spring.*"),
+            "rest", Pattern.compile(".*http.*|undertow|jetty"),
+            "rest-openapi", Pattern.compile(".*http.*|undertow|jetty"));
+
+    private static boolean delegatedTo(Set<String> used, String component) {
+        for (Map.Entry<String, Pattern> e : DELEGATES.entrySet()) {
+            if (used.contains(e.getKey()) && 
e.getValue().matcher(component).matches()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Adds the keys and the uri schemes of a template to the set; false when 
the template may use a component in a way
+     * the keys and schemes do not show.
+     */
+    private static boolean collectUsed(String key, Node node, Set<String> 
used) {
+        if (node instanceof MappingNode map) {
+            for (NodeTuple t : map.getValue()) {
+                String name = scalar(t.getKeyNode());
+                if ("beans".equals(name) || "bean".equals(name) || 
"method".equals(name)) {
+                    // a class of the Kamelet's own may need the artifact (the 
Kafka transform actions need camel:kafka)
+                    return false;
+                }
+                if (name != null) {
+                    used.add(name);
+                }
+                if (!collectUsed(name, t.getValueNode(), used)) {
+                    return false;
+                }
+            }
+        } else if (node instanceof SequenceNode seq) {
+            for (Node n : seq.getValue()) {
+                if (!collectUsed(key, n, used)) {
+                    return false;
+                }
+            }
+        } else if (node instanceof ScalarNode s) {
+            String v = s.getValue().trim();
+            if (v.contains("#class:") || v.contains("#type:")
+                    || v.startsWith("{{") && ("uri".equals(key) || 
ENDPOINT_KEYS.contains(key))) {
+                // a bean of a class, or an endpoint whose scheme is a property
+                return false;
+            }
+            Matcher m = URI_SCHEME.matcher(v);
+            if (m.find()) {
+                used.add(m.group(1));
+            } else if ("uri".equals(key) || ENDPOINT_KEYS.contains(key)) {
+                // uri: aws2-sqs with the options under parameters:, as the 
canonical form writes it
+                used.add(v.contains("?") ? v.substring(0, v.indexOf('?')) : v);
+            }
+        }
+        return true;
+    }
+
+    private static volatile Map<String, String> alternativeSchemes;
+
+    /** The alternative schemes of the components (https of http), each with 
the artifact of its component. */
+    private static Map<String, String> alternativeSchemes(CamelCatalog 
catalog) {
+        Map<String, String> answer = alternativeSchemes;
+        if (answer == null) {
+            answer = new HashMap<>();
+            for (String n : catalog.findComponentNames()) {
+                ComponentModel model = catalog.componentModel(n);
+                if (model != null && model.getAlternativeSchemes() != null) {
+                    for (String scheme : 
model.getAlternativeSchemes().split(",")) {
+                        answer.putIfAbsent(scheme.trim(), 
model.getArtifactId());
+                    }
+                }
+            }
+            alternativeSchemes = answer;
+        }
+        return answer;
+    }
+
+    private static CamelCatalog catalog() {
+        CamelCatalog catalog = dependencyCatalog;
+        if (catalog == null) {
+            catalog = new DefaultCamelCatalog();
+            dependencyCatalog = catalog;
+        }
+        return catalog;
+    }
+
     private static volatile CamelCatalog dependencyCatalog;
 
     /**
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecksTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecksTest.java
index 066e207ef06a..7f874fe6ee5c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecksTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/KameletChecksTest.java
@@ -287,6 +287,105 @@ class KameletChecksTest {
                             expression: "%s"
             """;
 
+    // CAMEL-25403: a camel: dependency the template does not use, as the 
model copied camel:timer from the source sample
+
+    private static String withDependencies(String kamelet, String... deps) {
+        StringBuilder sb = new StringBuilder("  dependencies:\n");
+        for (String d : deps) {
+            sb.append("    - \"").append(d).append("\"\n");
+        }
+        return kamelet.replace("  template:\n", sb + "  template:\n");
+    }
+
+    @Test
+    void aDependencyTheTemplateDoesNotUseIsANote() {
+        String content = withDependencies(TAG_KAMELET.formatted("${body} 
[{{tag}}]"), "camel:timer");
+        
assertThat(KameletChecks.unusedDependencies(content)).singleElement().asString()
+                .contains("camel:timer is not used by the template")
+                .contains("no timer: endpoint");
+        // a note, not an error: the Kamelet works, so the write is not refused
+        assertThat(KameletChecks.validateKameletFile(content)).isEmpty();
+    }
+
+    @Test
+    void theDependenciesTheTemplateUsesAreNotNoted() {
+        String source = """
+                apiVersion: camel.apache.org/v1
+                kind: Kamelet
+                metadata:
+                  name: order-source
+                  labels:
+                    camel.apache.org/kamelet.type: source
+                spec:
+                  definition:
+                    title: Order Source
+                    properties:
+                      period:
+                        title: Period
+                        type: integer
+                        default: 5000
+                  dependencies:
+                    - "camel:timer"
+                    - "camel:jq"
+                    - "camel:http"
+                    - "camel:kamelet"
+                    - "mvn:org.example:orders:1.0"
+                  template:
+                    from:
+                      uri: timer:orders
+                      parameters:
+                        period: "{{period}}"
+                      steps:
+                        - setBody:
+                            expression:
+                              jq:
+                                expression: '.'
+                        - to:
+                            uri: https://example.com/orders
+                        - to:
+                            uri: kamelet:sink
+                """;
+        assertThat(KameletChecks.unusedDependencies(source)).isEmpty();
+    }
+
+    @Test
+    void aTemplateThatMayUseAComponentUnseenIsNotNoted() {
+        String placeholderScheme = 
withDependencies(TAG_KAMELET.formatted("${body}"), "camel:kafka")
+                .replace("uri: kamelet:source", "uri: \"{{scheme}}:orders\"");
+        
assertThat(KameletChecks.unusedDependencies(placeholderScheme)).isEmpty();
+        String bean = withDependencies(TAG_KAMELET.formatted("${body}"), 
"camel:kafka")
+                .replace("    from:\n",
+                        "    beans:\n      - name: client\n        type: 
\"#class:org.example.Client\"\n    from:\n");
+        assertThat(KameletChecks.unusedDependencies(bean)).isEmpty();
+        // the Kafka transform actions of the catalog call a class that needs 
camel:kafka
+        String beanStep = withDependencies(TAG_KAMELET.formatted("${body}"), 
"camel:kafka")
+                .replace("        - setBody:\n",
+                        "        - bean:\n            beanType: 
org.example.HoistField\n        - setBody:\n");
+        assertThat(KameletChecks.unusedDependencies(beanStep)).isEmpty();
+    }
+
+    @Test
+    void aComponentThatRunsOnAnotherIsNotNoted() {
+        // cron runs on quartz, rest-openapi sends with an http component: 
neither is named by the template
+        String cron = withDependencies(TAG_KAMELET.formatted("${body}"), 
"camel:cron", "camel:quartz")
+                .replace("uri: kamelet:source", "uri: 
\"cron:tick?schedule=0/3+*+*+*+*+?\"");
+        assertThat(KameletChecks.unusedDependencies(cron)).isEmpty();
+        String rest = withDependencies(TAG_KAMELET.formatted("${body}"), 
"camel:rest-openapi", "camel:http")
+                .replace("        - setBody:\n", "        - to:\n            
uri: rest-openapi\n        - setBody:\n");
+        assertThat(KameletChecks.unusedDependencies(rest)).isEmpty();
+    }
+
+    @Test
+    void writingAKameletReportsTheNotes(@TempDir Path dir) {
+        String content = withDependencies(TAG_KAMELET.formatted("${body} 
[{{tag}}]"), "camel:timer");
+        JsonObject result = AuthoringTools.writeFile(new ToolContext(), dir, 
"tag-order-action.kamelet.yaml", content, true);
+        assertThat(result.getString("status")).isEqualTo("created");
+        assertThat(result.toJson()).contains("camel:timer is not used by the 
template");
+        JsonObject clean = AuthoringTools.writeFile(new ToolContext(), dir, 
"tag-order-action.kamelet.yaml",
+                TAG_KAMELET.formatted("${body} [{{tag}}]"), true);
+        assertThat(clean.containsKey("notes")).isFalse();
+    }
+
     @Test
     void aRightKameletFileIsValid(@TempDir Path dir) {
         String content = TAG_KAMELET.formatted("${body} [{{tag}}]");

Reply via email to