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

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


The following commit(s) were added to refs/heads/main by this push:
     new 167d537c739b CAMEL-26352: camel-diagram - mark the steps with a 
problem in a route diagram image (#28586)
167d537c739b is described below

commit 167d537c739b9dcf7faf6c50181e93f7016828cd
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 29 07:11:35 2026 +0100

    CAMEL-26352: camel-diagram - mark the steps with a problem in a route 
diagram image (#28586)
    
    camel cmd route-diagram of source files marks the steps that the checks of 
camel validate find a problem with: a yellow card border and a badge, and a 
callout beside the route (a numbered list below it for routes with branches). 
Option --problems, on by default; the camel_render_route_diagram MCP tool marks 
problems too.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../org/apache/camel/catalog/docs/diagram.adoc     |  15 ++
 .../camel-diagram/src/main/docs/diagram.adoc       |  15 ++
 .../src/main/docs/route-diagram-problems.png       | Bin 0 -> 73746 bytes
 .../camel/diagram/RouteDiagramLayoutEngine.java    | 132 +++++++++++++-
 .../apache/camel/diagram/RouteDiagramRenderer.java |  85 ++++++++-
 .../camel/diagram/RouteDiagramCalloutTest.java     | 190 ++++++++++++++++++++
 .../modules/ROOT/images/route-diagram-problems.png |   1 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   5 +
 .../camel-jbang-cmd-route-diagram.adoc             |   1 +
 .../META-INF/camel-jbang-commands-metadata.json    |   2 +-
 .../commands/action/CamelRouteDiagramAction.java   |  11 ++
 .../core/commands/action/RouteDiagramProblems.java | 173 ++++++++++++++++++
 .../commands/action/RouteDiagramProblemsTest.java  | 194 +++++++++++++++++++++
 .../jbang/core/commands/mcp/RouteDiagramTools.java |   2 +
 14 files changed, 820 insertions(+), 6 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/diagram.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/diagram.adoc
index e402b3e59d24..57ff8bb46125 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/diagram.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/diagram.adoc
@@ -203,6 +203,21 @@ camel cmd route-diagram myapp --output=myapp.png 
--body-types=false
 
 A diagram of a source file has no body types, as its routes have not run.
 
+=== Problems of the Steps
+
+A diagram of source files marks the steps that the checks of `camel validate` 
find a problem with: the card has a
+yellow border and a badge, and the problem is in a callout beside the route, 
at the height of the step. For example a
+Groovy step that reads fields of the file as it was read, before it is 
unmarshalled:
+
+image::ROOT:route-diagram-problems.png[The orders route: the setBody step 
reads body.items of the file as it was read, and a callout beside it says to 
unmarshal it first]
+
+In a route with branches (`choice`, `multicast`, `doTry`, ...) a callout 
beside the route would cross the steps of
+another branch, so the badge has a number, and the problems are a numbered 
list below the route.
+
+A YAML route is checked as `camel validate` does: the YAML DSL schema, the 
endpoints and expressions against the
+catalog, and the body flow. A Java or XML route is checked with the checks 
that read the source without compiling it,
+as the route diagram reads Java routes without compiling them. Turn the 
problems off with `--problems=false`.
+
 == ASCII Art Rendering
 
 The ASCII art renderer produces plain text diagrams using box-drawing 
characters.
diff --git a/components/camel-diagram/src/main/docs/diagram.adoc 
b/components/camel-diagram/src/main/docs/diagram.adoc
index e402b3e59d24..57ff8bb46125 100644
--- a/components/camel-diagram/src/main/docs/diagram.adoc
+++ b/components/camel-diagram/src/main/docs/diagram.adoc
@@ -203,6 +203,21 @@ camel cmd route-diagram myapp --output=myapp.png 
--body-types=false
 
 A diagram of a source file has no body types, as its routes have not run.
 
+=== Problems of the Steps
+
+A diagram of source files marks the steps that the checks of `camel validate` 
find a problem with: the card has a
+yellow border and a badge, and the problem is in a callout beside the route, 
at the height of the step. For example a
+Groovy step that reads fields of the file as it was read, before it is 
unmarshalled:
+
+image::ROOT:route-diagram-problems.png[The orders route: the setBody step 
reads body.items of the file as it was read, and a callout beside it says to 
unmarshal it first]
+
+In a route with branches (`choice`, `multicast`, `doTry`, ...) a callout 
beside the route would cross the steps of
+another branch, so the badge has a number, and the problems are a numbered 
list below the route.
+
+A YAML route is checked as `camel validate` does: the YAML DSL schema, the 
endpoints and expressions against the
+catalog, and the body flow. A Java or XML route is checked with the checks 
that read the source without compiling it,
+as the route diagram reads Java routes without compiling them. Turn the 
problems off with `--problems=false`.
+
 == ASCII Art Rendering
 
 The ASCII art renderer produces plain text diagrams using box-drawing 
characters.
diff --git a/components/camel-diagram/src/main/docs/route-diagram-problems.png 
b/components/camel-diagram/src/main/docs/route-diagram-problems.png
new file mode 100644
index 000000000000..5a1b940cf1e8
Binary files /dev/null and 
b/components/camel-diagram/src/main/docs/route-diagram-problems.png differ
diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
index 1f6f3f48db56..c2f9680e4e5f 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramLayoutEngine.java
@@ -56,6 +56,14 @@ public class RouteDiagramLayoutEngine {
      * label above the counters of the metrics. More with a larger font 
({@link #getBodyTypesGap()}).
      */
     public static final int BODY_TYPES_GAP = 30 * SCALE;
+    /** The space between a route and the callouts of its problems. */
+    public static final int CALLOUT_GAP = 16 * SCALE;
+    /** The space around the text of a callout. */
+    public static final int CALLOUT_PAD = 8 * SCALE;
+    /** The space before the text of an entry of the list of problems: room 
for its number. */
+    public static final int PROBLEM_NUMBER_WIDTH = 26 * SCALE;
+    // the end of a problem is often what to do about it, so a callout has 
room for most of it
+    private static final int MAX_CALLOUT_LINES = 6;
     private static final Pattern CARD_EXPRESSION = 
Pattern.compile("^([A-Za-z][\\w-]*)\\{(.*)\\}$", Pattern.DOTALL);
 
     public enum NodeLabelMode {
@@ -74,6 +82,7 @@ public class RouteDiagramLayoutEngine {
     private boolean tableLayout;
     private boolean cardStyle;
     private boolean bodyTypes;
+    private boolean problems;
     private final int bodyTypesGap;
     // the space between the steps of the route being laid out
     private int stepGap = V_GAP;
@@ -223,6 +232,24 @@ public class RouteDiagramLayoutEngine {
         return bodyTypes;
     }
 
+    /**
+     * Places the problems of the steps ({@link NodeInfo#problems}): in a 
callout beside the route, at the height of the
+     * step, or in a route with branches (where a callout would cross the 
steps of another branch) in a numbered list
+     * below the steps. Off by default.
+     */
+    public void setProblems(boolean problems) {
+        this.problems = problems;
+    }
+
+    public boolean isProblems() {
+        return problems;
+    }
+
+    /** The width of a callout of the problems of a step. */
+    public static int calloutWidth(int nodeWidth) {
+        return nodeWidth * 3 / 2;
+    }
+
     /**
      * The widest a label of body types can be: wider than a step, as the 
steps of two branches are one and a half steps
      * apart, and still apart from the label of a step beside it.
@@ -295,6 +322,11 @@ public class RouteDiagramLayoutEngine {
          * the class names, at most a few.
          */
         public List<String> bodyTypes = List.of();
+        /**
+         * The problems of the step, as the checks of the source report them, 
such as that a groovy step reads fields of
+         * the file as it was read: drawn in the diagram when it shows 
problems ({@link #setProblems}).
+         */
+        public List<String> problems = List.of();
         /**
          * The kinds of those bodies: null, payload, data and collection. More 
than one is a step that needs attention,
          * as a step that writes the body fails for the data, and one that 
reads fields fails for the payload.
@@ -353,6 +385,26 @@ public class RouteDiagramLayoutEngine {
         public RouteErrorFrame errorFrame;
         /** Whether the arrows have room for the body types of the steps 
({@link #setBodyTypes}). */
         public boolean bodyTypes;
+        /** The problems of the steps, in callouts beside the route or in a 
list below it ({@link #setProblems}). */
+        public final List<Callout> callouts = new ArrayList<>();
+        /**
+         * Whether the problems are a numbered list below the steps (a route 
with branches), not callouts beside them.
+         */
+        public boolean problemList;
+    }
+
+    /** The problems of a step: a callout beside the route, or an entry of the 
list below it. */
+    public static class Callout {
+        /** The step the problems are of. */
+        public LayoutNode node;
+        /** The number of the entry in the list of problems, from 1; 0 for a 
callout. */
+        public int number;
+        public int x;
+        public int y;
+        public int width;
+        public int height;
+        /** The text, wrapped to the width (the last line may need to be cut 
with an ellipsis). */
+        public List<String> lines;
     }
 
     public static TreeNode buildTree(List<NodeInfo> nodes) {
@@ -408,12 +460,16 @@ public class RouteDiagramLayoutEngine {
     }
 
     private List<String> wrapLabel(String label, int maxTextWidth) {
+        return wrapLabel(label, maxTextWidth, MAX_WRAP_LINES);
+    }
+
+    private List<String> wrapLabel(String label, int maxTextWidth, int 
maxLines) {
         if (fontMetrics.stringWidth(label) <= maxTextWidth) {
             return List.of(label);
         }
         List<String> lines = new ArrayList<>();
         String remaining = label;
-        while (!remaining.isEmpty() && lines.size() < MAX_WRAP_LINES) {
+        while (!remaining.isEmpty() && lines.size() < maxLines) {
             if (fontMetrics.stringWidth(remaining) <= maxTextWidth) {
                 lines.add(remaining);
                 remaining = "";
@@ -604,9 +660,83 @@ public class RouteDiagramLayoutEngine {
         lr.maxX = extent.maxX + overhang;
         lr.maxY = Math.max(lr.maxY, extent.maxY);
 
+        if (problems) {
+            layoutProblems(lr);
+        }
         return lr;
     }
 
+    private void layoutProblems(LayoutRoute lr) {
+        // in the order of the steps in the route (the order of the source)
+        List<LayoutNode> withProblems = lr.nodes.stream()
+                .filter(ln -> !ln.tableRow && ln.treeNode != null && 
!ln.treeNode.info.problems.isEmpty())
+                .toList();
+        if (withProblems.isEmpty()) {
+            return;
+        }
+        int lineHeight = fontMetrics.getHeight();
+        lr.problemList = lr.nodes.stream().anyMatch(ln -> 
isBranchingEip(ln.type));
+        if (!lr.problemList) {
+            // a column of callouts beside the route, each at the height of 
its step, below the one above it
+            int x = lr.maxX + CALLOUT_GAP;
+            int width = calloutWidth(nodeWidth);
+            int bottom = Integer.MIN_VALUE;
+            // without branches the order of the steps is from the top down
+            for (LayoutNode ln : withProblems) {
+                Callout c = new Callout();
+                c.node = ln;
+                c.lines = problemLines(ln.treeNode.info.problems, width - 2 * 
CALLOUT_PAD);
+                c.x = x;
+                c.width = width;
+                c.height = c.lines.size() * lineHeight + 2 * CALLOUT_PAD;
+                c.y = Math.max(ln.y + ln.height / 2 - c.height / 2, bottom + 
CALLOUT_PAD);
+                bottom = c.y + c.height;
+                lr.callouts.add(c);
+                lr.maxY = Math.max(lr.maxY, bottom);
+            }
+            lr.maxX = x + width;
+        } else {
+            // a numbered list below the steps, as wide as the route
+            int width = Math.max(lr.maxX - PADDING, nodeWidth * 2);
+            int y = lr.maxY + V_GAP / 2;
+            int number = 0;
+            for (LayoutNode ln : withProblems) {
+                Callout c = new Callout();
+                c.node = ln;
+                c.number = ++number;
+                c.lines = problemLines(ln.treeNode.info.problems, width - 
PROBLEM_NUMBER_WIDTH);
+                c.x = PADDING;
+                c.y = y;
+                c.width = width;
+                c.height = c.lines.size() * lineHeight;
+                y += c.height + CALLOUT_PAD;
+                lr.callouts.add(c);
+            }
+            lr.maxY = y;
+            lr.maxX = Math.max(lr.maxX, PADDING + width);
+        }
+    }
+
+    /**
+     * The problems wrapped to the width, each from a line of its own, at most 
six lines: a problem that does not fit
+     * has the rest of its text on its last line, which the renderer cuts with 
an ellipsis, and the problems after it
+     * are left out, which an ellipsis says. So a line can be wider than the 
width: the renderer draws each line cut to
+     * the width of the callout, and must keep doing so.
+     */
+    List<String> problemLines(List<String> problems, int width) {
+        List<String> lines = new ArrayList<>();
+        for (String problem : problems) {
+            int room = MAX_CALLOUT_LINES - lines.size();
+            if (room == 0) {
+                int last = lines.size() - 1;
+                lines.set(last, lines.get(last) + " …");
+                break;
+            }
+            lines.addAll(wrapLabel(problem, width, room));
+        }
+        return lines;
+    }
+
     private static void markTables(TreeNode node) {
         node.table = TABLE_EIPS.contains(node.info.type) && 
!node.children.isEmpty();
         for (TreeNode child : node.children) {
diff --git 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
index 552bcd3d570b..e3b79f1f7a39 100644
--- 
a/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
+++ 
b/components/camel-diagram/src/main/java/org/apache/camel/diagram/RouteDiagramRenderer.java
@@ -71,23 +71,24 @@ public class RouteDiagramRenderer {
 
     // the steps are cards (card, cardBorder, cardText for the kind, 
cardDetail for the detail) with an icon in a
     // circle of the color of the kind of step (from, to, eip, ...); text is 
the text in the nodes of the topology;
-    // warning, warningBg and warningText are the colors of the body types of 
a step reached with more than one kind
+    // warning, warningBg and warningText are the colors of the body types of 
a step reached with more than one kind,
+    // and of the problems of a step; badgeText is the mark in the warning 
circle of a problem
     private static final String DARK_COLORS
             = 
"bg=#0d1117:text=#f0f6fc:arrow=#656c76:label=#d1d7e0:from=#238636:to=#1f6feb:eip=#8957e5"
               + 
":choice=#d29922:default=#3d444d:transform=#1b7c83:processor=#bf4b8a"
               + 
":card=#151b23:cardBorder=#3d444d:cardText=#f0f6fc:cardDetail=#9198a1"
-              + ":warning=#d29922:warningBg=#272115:warningText=#e3b341";
+              + 
":warning=#d29922:warningBg=#272115:warningText=#e3b341:badgeText=#1f2328";
     private static final String LIGHT_COLORS
             = 
"bg=#f6f8fa:text=#f0f6fc:arrow=#59636e:label=#1f2328:from=#238636:to=#1f6feb:eip=#8957e5"
               + 
":choice=#d29922:default=#3d444d:transform=#1b7c83:processor=#bf4b8a"
               + 
":card=#ffffff:cardBorder=#d0d7de:cardText=#1f2328:cardDetail=#59636e"
-              + ":warning=#d4a72c:warningBg=#fff8c5:warningText=#7d4e00";
+              + 
":warning=#d4a72c:warningBg=#fff8c5:warningText=#7d4e00:badgeText=#1f2328";
     // white cards, readable on any background
     private static final String TRANSPARENT_COLORS
             = 
"bg=:text=#f0f6fc:arrow=#656c76:label=#d1d7e0:from=#238636:to=#1f6feb:eip=#8957e5"
               + 
":choice=#d29922:default=#3d444d:transform=#1b7c83:processor=#bf4b8a"
               + 
":card=#ffffff:cardBorder=#d0d7de:cardText=#1f2328:cardDetail=#59636e"
-              + ":warning=#d4a72c:warningBg=#fff8c5:warningText=#7d4e00";
+              + 
":warning=#d4a72c:warningBg=#fff8c5:warningText=#7d4e00:badgeText=#1f2328";
 
     private static final Map<String, String> COLOR_PRESETS = Map.of(
             "dark", DARK_COLORS,
@@ -144,6 +145,7 @@ public class RouteDiagramRenderer {
         private Color warning;
         private Color warningBg;
         private Color warningText;
+        private Color badgeText;
 
         /**
          * The colors of a theme: a preset (dark, light, transparent) or a 
spec of {@code key=color} pairs separated by
@@ -187,6 +189,7 @@ public class RouteDiagramRenderer {
             c.warning = parseColor(map.getOrDefault("warning", "#d29922"));
             c.warningBg = parseColor(map.getOrDefault("warningBg", "#272115"));
             c.warningText = parseColor(map.getOrDefault("warningText", 
"#e3b341"));
+            c.badgeText = parseColor(map.getOrDefault("badgeText", "#1f2328"));
             return c;
         }
 
@@ -291,6 +294,10 @@ public class RouteDiagramRenderer {
         public Color getWarningText() {
             return warningText;
         }
+
+        public Color getBadgeText() {
+            return badgeText;
+        }
     }
 
     public BufferedImage renderDiagram(
@@ -473,6 +480,76 @@ public class RouteDiagramRenderer {
         for (LayoutNode ln : lr.nodes) {
             drawNode(g, ln, colors, ln == lastHighlightedNode);
         }
+
+        for (RouteDiagramLayoutEngine.Callout c : lr.callouts) {
+            drawProblems(g, c, colors, lr.problemList, c.node == 
lastHighlightedNode);
+        }
+    }
+
+    /**
+     * The problems of a step ({@link RouteDiagramLayoutEngine#setProblems}): 
a warning border and a badge on its card,
+     * and their text in a callout beside the route, pointed at the card, or 
in an entry of the numbered list below the
+     * route, with the number of the badge.
+     */
+    private void drawProblems(
+            Graphics2D g, RouteDiagramLayoutEngine.Callout c, DiagramColors 
colors, boolean list,
+            boolean highlightFail) {
+        LayoutNode node = c.node;
+        if (!highlightFail) {
+            g.setColor(colors.getWarning());
+            g.setStroke(new BasicStroke(HIGHLIGHT_STROKE_WIDTH));
+            g.drawRoundRect(node.x, node.y, nodeWidth, node.height, ARC, ARC);
+        }
+        String mark = list ? Integer.toString(c.number) : "!";
+        drawBadge(g, node.x + nodeWidth - 2 * SCALE, node.y + 2 * SCALE, mark, 
colors);
+
+        Font font = new Font("SansSerif", Font.PLAIN, fontSizeNode);
+        FontMetrics fm = g.getFontMetrics(font);
+        int pad = RouteDiagramLayoutEngine.CALLOUT_PAD;
+        int textX;
+        int textWidth;
+        int baseline;
+        if (list) {
+            drawBadge(g, c.x + RouteDiagramLayoutEngine.PROBLEM_NUMBER_WIDTH / 
2, c.y + fm.getHeight() / 2, mark, colors);
+            textX = c.x + RouteDiagramLayoutEngine.PROBLEM_NUMBER_WIDTH;
+            textWidth = c.width - 
RouteDiagramLayoutEngine.PROBLEM_NUMBER_WIDTH;
+            baseline = c.y + fm.getAscent();
+            g.setColor(colors.getRouteLabel());
+        } else {
+            // the pointer from the card to its callout
+            int cardY = node.y + node.height / 2;
+            int pointY = Math.max(c.y + pad, Math.min(cardY, c.y + c.height - 
pad));
+            g.setColor(colors.getWarning());
+            g.setStroke(new BasicStroke(STROKE_WIDTH));
+            g.drawLine(node.x + nodeWidth, cardY, c.x, pointY);
+            g.setColor(colors.getWarningBg());
+            g.fillRoundRect(c.x, c.y, c.width, c.height, ARC, ARC);
+            g.setColor(colors.getWarning());
+            g.setStroke(new BasicStroke(BORDER_STROKE_WIDTH));
+            g.drawRoundRect(c.x, c.y, c.width, c.height, ARC, ARC);
+            textX = c.x + pad;
+            textWidth = c.width - 2 * pad;
+            baseline = c.y + pad + fm.getAscent();
+            g.setColor(colors.getWarningText());
+        }
+        g.setFont(font);
+        for (String line : c.lines) {
+            g.drawString(fit(fm, line, textWidth), textX, baseline);
+            baseline += fm.getHeight();
+        }
+        g.setStroke(new BasicStroke(STROKE_WIDTH));
+    }
+
+    /** A badge of a problem: a circle of the warning color with a mark ("!" 
or the number of the problem). */
+    private void drawBadge(Graphics2D g, int cx, int cy, String mark, 
DiagramColors colors) {
+        int d = 18 * SCALE;
+        g.setColor(colors.getWarning());
+        g.fillOval(cx - d / 2, cy - d / 2, d, d);
+        Font font = new Font("SansSerif", Font.BOLD, fontSizeNode - SCALE);
+        FontMetrics fm = g.getFontMetrics(font);
+        g.setFont(font);
+        g.setColor(colors.getBadgeText());
+        g.drawString(mark, cx - fm.stringWidth(mark) / 2, cy - fm.getHeight() 
/ 2 + fm.getAscent());
     }
 
     private void drawRoute(Graphics2D g, LayoutRoute lr, DiagramColors colors) 
{
diff --git 
a/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramCalloutTest.java
 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramCalloutTest.java
new file mode 100644
index 000000000000..23fb95948f07
--- /dev/null
+++ 
b/components/camel-diagram/src/test/java/org/apache/camel/diagram/RouteDiagramCalloutTest.java
@@ -0,0 +1,190 @@
+/*
+ * 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.diagram;
+
+import java.awt.Color;
+import java.awt.image.BufferedImage;
+import java.util.List;
+
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.Callout;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutNode;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.LayoutRoute;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+import org.apache.camel.diagram.RouteDiagramRenderer.DiagramColors;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The problems of the steps in a route diagram image: callouts beside a 
route, a numbered list below a route with
+ * branches.
+ */
+class RouteDiagramCalloutTest {
+
+    private static final String GROOVY = "groovy reads fields of the body 
(body.items), but the body here is the file as"
+                                         + " it was read (a GenericFile), not 
parsed data - unmarshal it first";
+
+    private static NodeInfo node(String type, String code, int level, 
String... problems) {
+        NodeInfo n = new NodeInfo();
+        n.type = type;
+        n.id = type + level + code.length();
+        n.code = code;
+        n.level = level;
+        n.problems = List.of(problems);
+        return n;
+    }
+
+    private static RouteInfo orders() {
+        RouteInfo r = new RouteInfo();
+        r.routeId = "orders";
+        r.nodes.add(node("route", "route[orders]", 0));
+        r.nodes.add(node("from", "from[file:orders]", 1));
+        r.nodes.add(node("setBody", "setBody[groovy{body.items}]", 2, GROOVY));
+        r.nodes.add(node("to", "to[http:pricing]", 2, "the http producer 
cannot send a Map"));
+        r.nodes.add(node("to", "to[jms:priced]", 2));
+        return r;
+    }
+
+    private static RouteInfo choice() {
+        RouteInfo r = new RouteInfo();
+        r.routeId = "choice";
+        r.nodes.add(node("route", "route[choice]", 0));
+        r.nodes.add(node("from", "from[timer:tick]", 1));
+        r.nodes.add(node("choice", "choice", 2));
+        r.nodes.add(node("when", "when[simple{${header.kind} == 0}]", 3));
+        r.nodes.add(node("to", "to[kafka:zero?brokerz=localhost]", 4, "kafka 
has no option brokerz"));
+        r.nodes.add(node("otherwise", "otherwise", 3));
+        r.nodes.add(node("marshal", "marshal[jackson]", 4, "the body is a 
String here"));
+        return r;
+    }
+
+    private static RouteDiagramLayoutEngine engine(boolean problems) {
+        RouteDiagramLayoutEngine engine = new RouteDiagramLayoutEngine();
+        engine.setCardStyle(true);
+        engine.setProblems(problems);
+        return engine;
+    }
+
+    @Test
+    void calloutsBesideARoute() {
+        RouteDiagramLayoutEngine engine = engine(true);
+        LayoutRoute lr = engine.layoutRoute(orders(), 
RouteDiagramLayoutEngine.PADDING);
+
+        assertThat(lr.problemList).isFalse();
+        assertThat(lr.callouts).hasSize(2);
+        Callout groovy = lr.callouts.get(0);
+        Callout http = lr.callouts.get(1);
+        assertThat(groovy.node.type).isEqualTo("setBody");
+        assertThat(groovy.number).isZero();
+        // right of the steps, the text wrapped to more than one line
+        int right = groovy.node.x + engine.getNodeWidth();
+        assertThat(groovy.x).isEqualTo(right + 
RouteDiagramLayoutEngine.CALLOUT_GAP);
+        assertThat(groovy.lines).hasSizeGreaterThan(1);
+        assertThat(String.join(" ", groovy.lines)).contains("GenericFile");
+        // one below the other, and room for them in the image
+        assertThat(http.y).isGreaterThanOrEqualTo(groovy.y + groovy.height);
+        assertThat(lr.maxX).isEqualTo(groovy.x + groovy.width);
+
+        // without problems shown, the route does not change
+        LayoutRoute plain = engine(false).layoutRoute(orders(), 
RouteDiagramLayoutEngine.PADDING);
+        assertThat(plain.callouts).isEmpty();
+        assertThat(plain.maxX).isLessThan(lr.maxX);
+    }
+
+    @Test
+    void numberedListBelowARouteWithBranches() {
+        RouteDiagramLayoutEngine engine = engine(true);
+        LayoutRoute plain = engine(false).layoutRoute(choice(), 
RouteDiagramLayoutEngine.PADDING);
+        LayoutRoute lr = engine.layoutRoute(choice(), 
RouteDiagramLayoutEngine.PADDING);
+
+        assertThat(lr.problemList).isTrue();
+        assertThat(lr.callouts).hasSize(2);
+        // numbered in the order of the steps in the route, below the steps
+        assertThat(lr.callouts.get(0).number).isEqualTo(1);
+        assertThat(lr.callouts.get(0).node.type).isEqualTo("to");
+        assertThat(lr.callouts.get(1).number).isEqualTo(2);
+        assertThat(lr.callouts.get(1).node.type).isEqualTo("marshal");
+        assertThat(lr.callouts.get(0).y).isGreaterThan(plain.maxY);
+        assertThat(lr.maxY).isGreaterThan(plain.maxY);
+    }
+
+    @Test
+    void eachProblemFromALineOfItsOwn() {
+        RouteDiagramLayoutEngine engine = engine(true);
+        int width = engine.getNodeWidth();
+        String first = "the first problem " + "of the step ".repeat(6);
+        String second = "the second problem " + "of the step ".repeat(12);
+        String third = "the third problem";
+
+        // two that fit: the second from a new line, never on a line of the 
first
+        List<String> two = engine.problemLines(List.of(third, second), width);
+        assertThat(two.get(0)).isEqualTo(third);
+        assertThat(two.get(1)).startsWith("the second problem");
+        assertThat(two).hasSizeLessThanOrEqualTo(6);
+
+        // more than six lines: the one cut has the rest of its own text on 
its last line, the next is left out
+        List<String> lines = engine.problemLines(List.of(first, second, 
third), width);
+        assertThat(lines).hasSize(6);
+        assertThat(lines).noneMatch(l -> l.contains("first") && 
l.contains("second"));
+        assertThat(lines).anyMatch(l -> l.startsWith("the second problem"));
+        assertThat(String.join(" ", lines)).doesNotContain("third");
+        assertThat(lines.get(5)).endsWith("…");
+    }
+
+    @Test
+    void drawTheNumberedList() {
+        DiagramColors colors = DiagramColors.parse("dark");
+        RouteDiagramLayoutEngine engine = engine(true);
+        LayoutRoute lr = engine.layoutRoute(choice(), 
RouteDiagramLayoutEngine.PADDING);
+        BufferedImage image = new 
RouteDiagramRenderer().renderDiagram(List.of(lr), lr.maxY, colors);
+
+        Callout kafka = lr.callouts.get(0);
+        assertThat(kafka.lines).hasSize(1);
+        // the badge with the number of the problem, in front of its text
+        int badgeX = kafka.x + RouteDiagramLayoutEngine.PROBLEM_NUMBER_WIDTH / 
2;
+        int badgeY = kafka.y + kafka.height / 2;
+        assertThat(new Color(image.getRGB(badgeX - 6 * 
RouteDiagramLayoutEngine.SCALE, badgeY)))
+                .isEqualTo(colors.getWarning());
+        // and the same badge on the card of its step, which has a warning 
border
+        LayoutNode step = kafka.node;
+        assertThat(new Color(image.getRGB(step.x + 1, step.y + step.height / 
2))).isEqualTo(colors.getWarning());
+        assertThat(new Color(
+                image.getRGB(step.x + engine.getNodeWidth() - 2 * 
RouteDiagramLayoutEngine.SCALE
+                             - 6 * RouteDiagramLayoutEngine.SCALE,
+                        step.y + 2 * RouteDiagramLayoutEngine.SCALE)))
+                .isEqualTo(colors.getWarning());
+    }
+
+    @Test
+    void drawTheProblems() {
+        DiagramColors colors = DiagramColors.parse("light");
+        RouteDiagramLayoutEngine engine = engine(true);
+        LayoutRoute lr = engine.layoutRoute(orders(), 
RouteDiagramLayoutEngine.PADDING);
+        BufferedImage image = new 
RouteDiagramRenderer().renderDiagram(List.of(lr), lr.maxY, colors);
+
+        Callout groovy = lr.callouts.get(0);
+        // the callout is filled with the warning background, at its right end 
where there is no text
+        assertThat(new Color(
+                image.getRGB(groovy.x + groovy.width - 4 * 
RouteDiagramLayoutEngine.SCALE,
+                        groovy.y + groovy.height / 2)))
+                .isEqualTo(colors.getWarningBg());
+        // the card of the step has a warning border
+        assertThat(new Color(image.getRGB(groovy.node.x + 1, groovy.node.y + 
groovy.node.height / 2)))
+                .isEqualTo(colors.getWarning());
+    }
+}
diff --git a/docs/components/modules/ROOT/images/route-diagram-problems.png 
b/docs/components/modules/ROOT/images/route-diagram-problems.png
new file mode 120000
index 000000000000..0c43b836a791
--- /dev/null
+++ b/docs/components/modules/ROOT/images/route-diagram-problems.png
@@ -0,0 +1 @@
+../../../../../components/camel-diagram/src/main/docs/route-diagram-problems.png
\ No newline at end of file
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index 1b930324f724..f571efbce259 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -2685,6 +2685,11 @@ A program that lays out routes with 
`RouteDiagramLayoutEngine` and draws them wi
 profile) shows them on the arrows, with longer arrows to hold them. Use 
`--body-types=false` for the diagram without
 them.
 
+`camel cmd route-diagram` of source files, and the 
`camel_render_route_diagram` MCP tool, check the routes as
+`camel validate` does, and mark the steps with a problem: a yellow border and 
a callout beside the route (a numbered
+list below a route with branches). An image of routes with problems is wider 
or taller. Use `--problems=false` for the
+diagram without them.
+
 `camel cmd route-diagram --output=diagram.png` without `--theme` now saves the 
PNG image in the `light` theme. Before,
 the default theme was `unicode` also for a file, so the command saved the text 
diagram in `diagram.txt` instead. A
 diagram in the terminal is still text (`unicode`) by default, and a file that 
does not end with `.png` still gets the
diff --git 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
index 6becab7fd9e1..55c31b82ebd6 100644
--- 
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
+++ 
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-cmd-route-diagram.adoc
@@ -32,6 +32,7 @@ camel cmd route-diagram [options]
 | `--metric` | Whether to include live metrics (only possible for running 
Camel application) | true | boolean
 | `--node-label` | What text to display in diagram nodes: code, description, 
or both (default) | both | String
 | `--output` | Save diagram to a file (PNG for image themes, text for ascii 
theme) |  | String
+| `--problems` | Whether to mark the steps that the checks of camel validate 
find a problem with, and say what it is beside the route, in an image of source 
files | true | boolean
 | `--theme` | Color theme preset (dark, light, transparent, ascii, unicode) or 
custom colors (e.g. bg=#1e1e1e:from=#2e7d32:to=#1565c0). Values can be #hex or 
ANSI color names (e.g. from=seagreen:to=steelblue). Use bg= for transparent. 
Use ascii/unicode for plain text output. Can also be set via DIAGRAM_COLORS env 
var. Default: unicode in the terminal, and light when --output is a .png file. 
|  | String
 | `--timeout` | How long to wait for the integration to answer, in millis or 
as a duration such as 30s | 10000 | long
 | `--watch` | Execute periodically and showing output fullscreen |  | boolean
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index 7baca55b3dfa..b85749921ba7 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -3,7 +3,7 @@
     { "name": "ask", "fullName": "ask", "description": "Ask a question about a 
running Camel application using AI", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names": 
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY, 
OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", "javaType": 
"java.lang.String", "type": "string" }, { "names": "--api-type", "description": 
"API type: 'ollama', 'openai', 'anthropic', or 'watsonx'", "javaType":  [...]
     { "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind 
source and sink Kamelets as a new Camel integration", "deprecated": true, 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options": 
[ { "names": "--error-handler", "description": "Add error handler 
(none|log|sink:<endpoint>). Sink endpoints are expected in the format 
[[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet 
name.", "javaType": "java.lang.String", "type": "stri [...]
     { "name": "catalog", "fullName": "catalog", "description": "List artifacts 
from Camel Catalog", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [ 
{ "names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"component", "fullName": "catalog component", "description": "List components 
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
-    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
+    { "name": "cmd", "fullName": "cmd", "description": "Performs commands in 
the running Camel integrations, such as start\/stop route, or change logging 
levels.", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": 
"browse", "fullName": "cmd browse", "description": "Browse pending messages on 
endpoints [...]
     { "name": "completion", "fullName": "completion", "description": "Generate 
completion script for bash\/zsh", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names": 
"-h,--help", "description": "Display the help and sub-commands", "javaType": 
"boolean", "type": "boolean" } ] },
     { "name": "config", "fullName": "config", "description": "Get and set user 
configuration values", "sourceClass": 
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ { 
"names": "-h,--help", "description": "Display the help and sub-commands", 
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get", 
"fullName": "config get", "description": "Display user configuration value", 
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
     { "name": "debug", "fullName": "debug", "description": "Debug local Camel 
integration", "sourceClass": "org.apache.camel.dsl.jbang.core.commands.Debug", 
"options": [ { "names": "--ago", "description": "Use ago instead of yyyy-MM-dd 
HH:mm:ss in timestamp.", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background", "description": "Run in the background", "defaultValue": 
"false", "javaType": "boolean", "type": "boolean" }, { "names": 
"--background-wait", "description": "How [...]
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
index 4c84f85f7c09..c35b2db4d625 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteDiagramAction.java
@@ -131,6 +131,11 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
                                       + " history recorded them, such as in 
the dev profile)")
     boolean bodyTypes;
 
+    @CommandLine.Option(names = { "--problems" }, defaultValue = "true", arity 
= "0..1", fallbackValue = "true",
+                        description = "Whether to mark the steps that the 
checks of camel validate find a problem with, and"
+                                      + " say what it is beside the route, in 
an image of source files")
+    boolean problems;
+
     @CommandLine.Option(names = { "--highlight" },
                         description = "Comma-separated node IDs to highlight 
in the diagram")
     String highlight;
@@ -229,6 +234,9 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
             if (routes == null) {
                 return 1;
             }
+            if (problems && !isTextTheme()) {
+                RouteDiagramProblems.attach(routes, files);
+            }
         } else {
             // more than one match is an error that says which integrations 
match
             CamelProcess process = processes.size() > 1 ? 
ProcessFinder.requireOne(nameOrPid) : processes.get(0);
@@ -283,6 +291,8 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
         // an image draws the steps as cards: a title and its detail beside an 
icon
         engine.setCardStyle(!isTextTheme());
         engine.setBodyTypes(bodyTypes && !isTextTheme());
+        // the problems are those of the source files, checked without running 
them
+        engine.setProblems(problems && sourceMode && !isTextTheme());
 
         List<LayoutRoute> layoutRoutes = new ArrayList<>();
         int currentY = RouteDiagramLayoutEngine.PADDING;
@@ -543,6 +553,7 @@ public class CamelRouteDiagramAction extends 
ActionWatchCommand {
             this.nodeLabel = nodeLabel;
         }
         this.metric = false;
+        this.problems = true;
         return doCall();
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteDiagramProblems.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteDiagramProblems.java
new file mode 100644
index 000000000000..a5cb33ba8e34
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/RouteDiagramProblems.java
@@ -0,0 +1,173 @@
+/*
+ * 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.action;
+
+import java.io.IOException;
+import java.nio.charset.CharacterCodingException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.camel.catalog.CamelCatalog;
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+import org.apache.camel.dsl.jbang.core.commands.ai.RouteAssist;
+import org.apache.camel.dsl.jbang.core.commands.ai.SourceValidator;
+
+/**
+ * The problems of the source files of a route diagram, on the steps they are 
of: the checks of {@code camel validate}
+ * for a YAML route, and for Java and XML the checks that read the source 
without compiling it ({@link RouteAssist}), as
+ * the route diagram reads Java routes without compiling them.
+ */
+final class RouteDiagramProblems {
+
+    /** A problem of a line of the source; line 0 for a problem of the file. */
+    record Problem(int line, String text) {
+    }
+
+    /** The checks of a source file: {@link #problems}. */
+    @FunctionalInterface
+    interface Checks {
+        List<Problem> problems(String name, String content, CamelCatalog 
catalog, Path directory);
+    }
+
+    private static final Pattern MESSAGE_LINE = Pattern.compile("^Line (\\d+): 
(.*)$", Pattern.DOTALL);
+    // the step and route a message names first, which the diagram shows 
already: "setBody: route orders: "
+    private static final Pattern STEP_PREFIX = 
Pattern.compile("^[A-Za-z][\\w-]*: route [^:\\s]+: ");
+
+    private RouteDiagramProblems() {
+    }
+
+    /**
+     * Puts the problems of the source files on the steps of their routes 
({@link NodeInfo#problems}): a problem on the
+     * step whose line is the last one before it (the line of the option or 
expression that is wrong is inside its
+     * step), a problem of the file on the first step: also that the file 
cannot be read or checked.
+     */
+    static void attach(List<RouteInfo> routes, List<String> files) {
+        attach(routes, files, RouteDiagramProblems::problems);
+    }
+
+    static void attach(List<RouteInfo> routes, List<String> files, Checks 
checks) {
+        CamelCatalog catalog = null;
+        for (String file : files) {
+            Path path = Path.of(file);
+            if (!Files.isRegularFile(path)) {
+                continue;
+            }
+            String name = path.getFileName().toString();
+            List<NodeInfo> nodes = routes.stream()
+                    .filter(r -> sourceOf(r, name))
+                    .flatMap(r -> r.nodes.stream())
+                    .toList();
+            if (nodes.isEmpty()) {
+                continue;
+            }
+            String content;
+            try {
+                content = Files.readString(path);
+            } catch (IOException e) {
+                // the diagram shows the routes of the file, so it says why 
their problems are not there
+                add(nodes.get(0), "the file cannot be read to check it: "
+                                  + (e instanceof CharacterCodingException ? 
"it is not UTF-8 text" : e.getMessage()));
+                continue;
+            }
+            if (catalog == null) {
+                catalog = new DefaultCamelCatalog();
+            }
+            List<Problem> problems;
+            try {
+                problems = checks.problems(name, content, catalog, 
path.toAbsolutePath().getParent());
+            } catch (RuntimeException e) {
+                // a check that fails (a parser bug on a broken file) must not 
fail the diagram, which is drawn anyway
+                String message = e.getMessage();
+                add(nodes.get(0), "the file could not be checked: "
+                                  + (message != null && !message.isBlank() ? 
text(message) : e.getClass().getSimpleName()));
+                continue;
+            }
+            for (Problem p : problems) {
+                add(stepAt(nodes, p.line()), p.text());
+            }
+        }
+    }
+
+    private static void add(NodeInfo node, String problem) {
+        List<String> list = new ArrayList<>(node.problems);
+        list.add(problem);
+        node.problems = List.copyOf(list);
+    }
+
+    /** The problems of a source file. */
+    static List<Problem> problems(String name, String content, CamelCatalog 
catalog, Path directory) {
+        String lower = name.toLowerCase(Locale.ROOT);
+        List<Problem> answer = new ArrayList<>();
+        if (lower.endsWith(".yaml") || lower.endsWith(".yml")) {
+            for (String msg : SourceValidator.validate(name, content, catalog, 
null, directory)) {
+                answer.add(problem(msg));
+            }
+        } else if (RouteAssist.supports(name, content)) {
+            for (RouteAssist.Diagnostic d : RouteAssist.diagnostics(name, 
content, catalog, directory)) {
+                if (d.severity() == RouteAssist.Severity.ERROR) {
+                    answer.add(new Problem(d.line(), text(d.message())));
+                }
+            }
+        }
+        return answer;
+    }
+
+    /** A message of the validator, "Line 7: setBody: route orders: groovy 
reads ...", as a problem. */
+    static Problem problem(String message) {
+        Matcher m = MESSAGE_LINE.matcher(message);
+        if (m.matches()) {
+            return new Problem(Integer.parseInt(m.group(1)), text(m.group(2)));
+        }
+        return new Problem(0, text(message));
+    }
+
+    /**
+     * The text of a problem for a callout: its first line (more lines are the 
source to write, which the diagram has no
+     * room for), without the step and route it names first, which the callout 
points at.
+     */
+    static String text(String message) {
+        String text = message.strip();
+        int nl = text.indexOf('\n');
+        if (nl > 0) {
+            text = text.substring(0, nl).strip();
+        }
+        return STEP_PREFIX.matcher(text).replaceFirst("");
+    }
+
+    /** The step of a line: the last step that starts at or before it, or the 
first step. */
+    static NodeInfo stepAt(List<NodeInfo> nodes, int line) {
+        NodeInfo answer = nodes.get(0);
+        for (NodeInfo n : nodes) {
+            if (n.line > 0 && n.line <= line && n.line >= answer.line) {
+                answer = n;
+            }
+        }
+        return answer;
+    }
+
+    /** Whether the route comes from the file: its source is the file name, or 
the class name of a Java file. */
+    private static boolean sourceOf(RouteInfo route, String fileName) {
+        return route.source != null && (route.source.equals(fileName) || 
fileName.startsWith(route.source + "."));
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteDiagramProblemsTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteDiagramProblemsTest.java
new file mode 100644
index 000000000000..8eef66d3cb04
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/RouteDiagramProblemsTest.java
@@ -0,0 +1,194 @@
+/*
+ * 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.action;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.NodeInfo;
+import org.apache.camel.diagram.RouteDiagramLayoutEngine.RouteInfo;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class RouteDiagramProblemsTest {
+
+    @TempDir
+    Path dir;
+
+    private static NodeInfo node(String type, String code, int level, int 
line) {
+        NodeInfo n = new NodeInfo();
+        n.type = type;
+        n.code = code;
+        n.level = level;
+        n.line = line;
+        return n;
+    }
+
+    @Test
+    void textOfAProblem() {
+        // the step and route it names first are where the callout points
+        assertThat(RouteDiagramProblems.text("setBody: route orders: groovy 
reads fields of the body"))
+                .isEqualTo("groovy reads fields of the body");
+        // the lines to write after it are left out
+        assertThat(RouteDiagramProblems.text("an option at the column of its 
EIP\n  - to:\n      uri: log:x"))
+                .isEqualTo("an option at the column of its EIP");
+        assertThat(RouteDiagramProblems.problem("Line 7: setBody: route 
orders: groovy reads fields"))
+                .isEqualTo(new RouteDiagramProblems.Problem(7, "groovy reads 
fields"));
+        assertThat(RouteDiagramProblems.problem("the file is empty"))
+                .isEqualTo(new RouteDiagramProblems.Problem(0, "the file is 
empty"));
+    }
+
+    @Test
+    void stepOfALine() {
+        List<NodeInfo> nodes = List.of(node("route", "route[orders]", 0, 4), 
node("from", "from[file:orders]", 1, 4),
+                node("setBody", "setBody[groovy{body.items}]", 2, 6), 
node("to", "to[http:pricing]", 2, 9));
+        // the line of the expression is inside its step
+        assertThat(RouteDiagramProblems.stepAt(nodes, 
7).type).isEqualTo("setBody");
+        assertThat(RouteDiagramProblems.stepAt(nodes, 9).type).isEqualTo("to");
+        assertThat(RouteDiagramProblems.stepAt(nodes, 
4).type).isEqualTo("from");
+        // before the first step, or of the file: the first step
+        assertThat(RouteDiagramProblems.stepAt(nodes, 
1).type).isEqualTo("route");
+        assertThat(RouteDiagramProblems.stepAt(nodes, 
0).type).isEqualTo("route");
+    }
+
+    @Test
+    void problemsOfAYamlRoute() throws Exception {
+        Path file = dir.resolve("orders.camel.yaml");
+        Files.writeString(file, """
+                - route:
+                    id: orders
+                    from:
+                      uri: file:orders
+                      steps:
+                        - setBody:
+                            groovy:
+                              expression: body.items
+                        - to:
+                            uri: log:orders
+                """);
+        RouteInfo route = new RouteInfo();
+        route.routeId = "orders";
+        route.source = "orders.camel.yaml";
+        route.nodes.add(node("route", "route[orders]", 0, 4));
+        route.nodes.add(node("from", "from[file:orders]", 1, 4));
+        route.nodes.add(node("setBody", "setBody[groovy{body.items}]", 2, 6));
+        route.nodes.add(node("to", "to[log:orders]", 2, 9));
+
+        RouteDiagramProblems.attach(List.of(route), List.of(file.toString()));
+
+        // groovy reads fields of the file as it was read
+        assertThat(route.nodes.get(2).problems).singleElement().asString()
+                
.contains("body.items").contains("GenericFile").doesNotStartWith("setBody: 
route");
+        assertThat(route.nodes.get(0).problems).isEmpty();
+        assertThat(route.nodes.get(3).problems).isEmpty();
+    }
+
+    @Test
+    void problemsOfAJavaRoute() throws Exception {
+        Path file = dir.resolve("MyRoute.java");
+        Files.writeString(file, """
+                import org.apache.camel.builder.RouteBuilder;
+
+                public class MyRoute extends RouteBuilder {
+                    @Override
+                    public void configure() {
+                        from("timer:tick")
+                            .log("tick")
+                            .to("kafka:orders?brokerz=localhost:9092");
+                    }
+                }
+                """);
+        RouteInfo route = new RouteInfo();
+        route.routeId = "route1";
+        route.source = "MyRoute";
+        route.nodes.add(node("route", "route[route1]", 0, 6));
+        route.nodes.add(node("from", "from[timer:tick]", 1, 6));
+        route.nodes.add(node("log", "log[tick]", 2, 7));
+        route.nodes.add(node("to", "to[kafka:orders?brokerz=localhost:9092]", 
2, 8));
+
+        RouteDiagramProblems.attach(List.of(route), List.of(file.toString()));
+
+        // the option the kafka component does not have, found without 
compiling the source
+        
assertThat(route.nodes.get(3).problems).singleElement().asString().contains("brokerz");
+        assertThat(route.nodes.get(2).problems).isEmpty();
+    }
+
+    @Test
+    void aFileThatCannotBeRead() throws Exception {
+        Path file = dir.resolve("orders.camel.yaml");
+        // not UTF-8 text
+        Files.write(file, new byte[] { '-', ' ', (byte) 0xff, (byte) 0xfe, 
'\n' });
+        RouteInfo route = new RouteInfo();
+        route.routeId = "orders";
+        route.source = "orders.camel.yaml";
+        route.nodes.add(node("route", "route[orders]", 0, 1));
+        route.nodes.add(node("from", "from[file:orders]", 1, 2));
+
+        RouteDiagramProblems.attach(List.of(route), List.of(file.toString()));
+
+        // on the first step, as a problem of the file, not as a file without 
problems
+        assertThat(route.nodes.get(0).problems)
+                .containsExactly("the file cannot be read to check it: it is 
not UTF-8 text");
+        assertThat(route.nodes.get(1).problems).isEmpty();
+    }
+
+    @Test
+    void aCheckThatFails() throws Exception {
+        Path file = dir.resolve("orders.camel.yaml");
+        Files.writeString(file, "- route:\n    id: orders\n");
+        Path other = dir.resolve("other.camel.yaml");
+        Files.writeString(other, "- route:\n    id: other\n");
+        RouteInfo route = new RouteInfo();
+        route.routeId = "orders";
+        route.source = "orders.camel.yaml";
+        route.nodes.add(node("route", "route[orders]", 0, 1));
+        route.nodes.add(node("from", "from[file:orders]", 1, 2));
+        RouteInfo otherRoute = new RouteInfo();
+        otherRoute.routeId = "other";
+        otherRoute.source = "other.camel.yaml";
+        otherRoute.nodes.add(node("route", "route[other]", 0, 1));
+
+        RouteDiagramProblems.attach(List.of(route, otherRoute), 
List.of(file.toString(), other.toString()),
+                (name, content, catalog, directory) -> {
+                    throw name.startsWith("orders") ? new 
IllegalStateException("parser bug") : new NullPointerException();
+                });
+
+        // the diagram is still drawn, and says why the problems of the file 
are not there
+        assertThat(route.nodes.get(0).problems).containsExactly("the file 
could not be checked: parser bug");
+        assertThat(route.nodes.get(1).problems).isEmpty();
+        // the next file is checked too; an exception without a message is 
named by its class
+        assertThat(otherRoute.nodes.get(0).problems)
+                .containsExactly("the file could not be checked: 
NullPointerException");
+    }
+
+    @Test
+    void aFileWithoutRoutesOfTheDiagramIsNotChecked() throws Exception {
+        Path file = dir.resolve("other.camel.yaml");
+        Files.writeString(file, "- route:\n    from:\n      uri: nope:\n");
+        RouteInfo route = new RouteInfo();
+        route.routeId = "orders";
+        route.source = "orders.camel.yaml";
+        route.nodes.add(node("route", "route[orders]", 0, 1));
+
+        RouteDiagramProblems.attach(List.of(route), List.of(file.toString(), 
dir.resolve("missing.yaml").toString()));
+
+        assertThat(route.nodes.get(0).problems).isEmpty();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
index 6c810a18bda2..4207f1d6eb9b 100644
--- 
a/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-mcp/src/main/java/org/apache/camel/dsl/jbang/core/commands/mcp/RouteDiagramTools.java
@@ -46,6 +46,8 @@ public class RouteDiagramTools {
                         + "Java routes are read by the Java DSL parser, 
without compiling them; a step it cannot "
                         + "work out (a lambda, a processor) is shown as a 
placeholder, and the parts it could not read "
                         + "are listed in the message. "
+                        + "An image marks the steps that the checks of camel 
validate find a problem with, and says "
+                        + "what the problem is beside the route. "
                         + "For image themes (light, dark, transparent) the PNG 
is written to disk and the absolute path "
                         + "is returned. For text themes (ascii, unicode) the 
diagram is returned directly as text so "
                         + "it can be read and understood by the caller. "

Reply via email to