fix: use root tree for compound node detection instead of flat nodes list
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 1m4s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped

The agent now sends shallow copies (without children) in the flat nodes
list. Build nodeById map by walking graph.getRoot() tree which preserves
children, falling back to flat list via putIfAbsent for compatibility.

Also adds EIP_FILTER, EIP_IDEMPOTENT_CONSUMER, EIP_RECIPIENT_LIST as
new compound container types per updated DIAGRAMS.md.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-29 19:34:40 +02:00
parent 0ec41bc02c
commit 7e968dc06b
2 changed files with 50 additions and 30 deletions

View File

@@ -109,7 +109,8 @@ public class ElkDiagramRenderer implements DiagramRenderer {
NodeType.DO_TRY, NodeType.DO_CATCH, NodeType.DO_FINALLY, NodeType.DO_TRY, NodeType.DO_CATCH, NodeType.DO_FINALLY,
NodeType.EIP_LOOP, NodeType.EIP_MULTICAST, NodeType.EIP_LOOP, NodeType.EIP_MULTICAST,
NodeType.EIP_AGGREGATE, NodeType.ON_EXCEPTION, NodeType.ERROR_HANDLER, NodeType.EIP_AGGREGATE, NodeType.ON_EXCEPTION, NodeType.ERROR_HANDLER,
NodeType.ON_COMPLETION, NodeType.EIP_CIRCUIT_BREAKER NodeType.ON_COMPLETION, NodeType.EIP_CIRCUIT_BREAKER,
NodeType.EIP_FILTER, NodeType.EIP_IDEMPOTENT_CONSUMER, NodeType.EIP_RECIPIENT_LIST
); );
/** Top-level handler types laid out in their own separate ELK graph. */ /** Top-level handler types laid out in their own separate ELK graph. */
@@ -185,15 +186,16 @@ public class ElkDiagramRenderer implements DiagramRenderer {
// ---------------------------------------------------------------- // ----------------------------------------------------------------
private LayoutResult computeLayout(RouteGraph graph, Direction rootDirection) { private LayoutResult computeLayout(RouteGraph graph, Direction rootDirection) {
LayoutContext ctx = new LayoutContext(ElkGraphFactory.eINSTANCE); // 1. Build node index from root tree (preserves children) with flat-list fallback
// 1. Partition graph nodes into main flow vs handler sections
Map<String, RouteNode> nodeById = buildNodeIndex(graph); Map<String, RouteNode> nodeById = buildNodeIndex(graph);
LayoutContext ctx = new LayoutContext(ElkGraphFactory.eINSTANCE, nodeById);
// 2. Partition graph nodes into main flow vs handler sections
List<RouteNode> mainNodes = new ArrayList<>(); List<RouteNode> mainNodes = new ArrayList<>();
List<RouteNode> handlerNodes = new ArrayList<>(); List<RouteNode> handlerNodes = new ArrayList<>();
partitionNodes(graph, nodeById, mainNodes, handlerNodes); partitionNodes(graph, nodeById, mainNodes, handlerNodes);
// 2. Build ELK graphs — main flow + separate handler roots // 3. Build ELK graphs — main flow + separate handler roots
ElkNode rootNode = createElkRoot("root", rootDirection, 1.0, ctx); ElkNode rootNode = createElkRoot("root", rootDirection, 1.0, ctx);
for (RouteNode rn : mainNodes) { for (RouteNode rn : mainNodes) {
if (!ctx.elkNodeMap.containsKey(rn.getId())) { if (!ctx.elkNodeMap.containsKey(rn.getId())) {
@@ -208,34 +210,53 @@ public class ElkDiagramRenderer implements DiagramRenderer {
handlerRoots.add(hr); handlerRoots.add(hr);
} }
// 3. Create ELK edges (filtering DO_TRY internals and cross-root edges) // 4. Create ELK edges (filtering DO_TRY internals and cross-root edges)
createElkEdges(graph, ctx); createElkEdges(graph, ctx);
// 4. Run layout engine // 5. Run layout engine
RecursiveGraphLayoutEngine engine = new RecursiveGraphLayoutEngine(); RecursiveGraphLayoutEngine engine = new RecursiveGraphLayoutEngine();
engine.layout(rootNode, new BasicProgressMonitor()); engine.layout(rootNode, new BasicProgressMonitor());
for (ElkNode hr : handlerRoots) { for (ElkNode hr : handlerRoots) {
engine.layout(hr, new BasicProgressMonitor()); engine.layout(hr, new BasicProgressMonitor());
} }
// 5. Post-process: fix DO_TRY section ordering and widths // 6. Post-process: fix DO_TRY section ordering and widths
postProcessDoTrySections(ctx); postProcessDoTrySections(ctx);
// 6. Extract positioned result // 7. Extract positioned result
return extractLayout(graph, rootNode, handlerRoots, ctx); return extractLayout(graph, rootNode, handlerRoots, ctx);
} }
/** Build a lookup from the flat nodes list (deduplicates nested + top-level entries). */ /**
* Build a node lookup by walking the root tree (which preserves children for
* compound nodes). Falls back to the flat nodes list for any nodes not in the
* tree (backward compatibility when root is not set).
*/
private Map<String, RouteNode> buildNodeIndex(RouteGraph graph) { private Map<String, RouteNode> buildNodeIndex(RouteGraph graph) {
Map<String, RouteNode> nodeById = new HashMap<>(); Map<String, RouteNode> nodeById = new HashMap<>();
if (graph.getRoot() != null) {
collectNodesRecursive(graph.getRoot(), nodeById);
}
if (graph.getNodes() != null) { if (graph.getNodes() != null) {
for (RouteNode rn : graph.getNodes()) { for (RouteNode rn : graph.getNodes()) {
nodeById.put(rn.getId(), rn); nodeById.putIfAbsent(rn.getId(), rn);
} }
} }
return nodeById; return nodeById;
} }
/** Recursively collect RouteNodes from the tree into a map (preserves children). */
private void collectNodesRecursive(RouteNode node, Map<String, RouteNode> map) {
if (node.getId() != null) {
map.put(node.getId(), node);
}
if (node.getChildren() != null) {
for (RouteNode child : node.getChildren()) {
collectNodesRecursive(child, map);
}
}
}
/** /**
* Separate main flow nodes from handler sections by walking FLOW edges * Separate main flow nodes from handler sections by walking FLOW edges
* from the graph root. Handler sections (onException, onCompletion, etc.) * from the graph root. Handler sections (onException, onCompletion, etc.)
@@ -266,16 +287,14 @@ public class ElkDiagramRenderer implements DiagramRenderer {
} }
Set<String> seen = new HashSet<>(); Set<String> seen = new HashSet<>();
if (graph.getNodes() != null) { for (RouteNode rn : nodeById.values()) {
for (RouteNode rn : graph.getNodes()) { if (seen.contains(rn.getId())) continue;
if (seen.contains(rn.getId())) continue; seen.add(rn.getId());
seen.add(rn.getId()); if (rn.getType() != null && HANDLER_SECTION_TYPES.contains(rn.getType())
if (rn.getType() != null && HANDLER_SECTION_TYPES.contains(rn.getType()) && rn.getChildren() != null && !rn.getChildren().isEmpty()) {
&& rn.getChildren() != null && !rn.getChildren().isEmpty()) { handlerNodes.add(rn);
handlerNodes.add(rn); } else if (mainNodeIds.isEmpty() || mainNodeIds.contains(rn.getId())) {
} else if (mainNodeIds.isEmpty() || mainNodeIds.contains(rn.getId())) { mainNodes.add(rn);
mainNodes.add(rn);
}
} }
} }
} }
@@ -378,15 +397,13 @@ public class ElkDiagramRenderer implements DiagramRenderer {
/** Extract positioned nodes, edges, and bounding box from the ELK layout result. */ /** Extract positioned nodes, edges, and bounding box from the ELK layout result. */
private LayoutResult extractLayout(RouteGraph graph, ElkNode rootNode, private LayoutResult extractLayout(RouteGraph graph, ElkNode rootNode,
List<ElkNode> handlerRoots, LayoutContext ctx) { List<ElkNode> handlerRoots, LayoutContext ctx) {
// Extract positioned nodes // Extract positioned nodes (uses tree-aware nodeById for compound children)
List<PositionedNode> positionedNodes = new ArrayList<>(); List<PositionedNode> positionedNodes = new ArrayList<>();
if (graph.getNodes() != null) { for (RouteNode rn : ctx.nodeById.values()) {
for (RouteNode rn : graph.getNodes()) { if (ctx.childNodeIds.contains(rn.getId())) continue;
if (ctx.childNodeIds.contains(rn.getId())) continue; ElkNode elkNode = ctx.elkNodeMap.get(rn.getId());
ElkNode elkNode = ctx.elkNodeMap.get(rn.getId()); if (elkNode == null) continue;
if (elkNode == null) continue; positionedNodes.add(extractPositionedNode(rn, elkNode, getElkRoot(elkNode), ctx));
positionedNodes.add(extractPositionedNode(rn, elkNode, getElkRoot(elkNode), ctx));
}
} }
// Extract edges from main root + handler roots // Extract edges from main root + handler roots
@@ -977,6 +994,7 @@ public class ElkDiagramRenderer implements DiagramRenderer {
/** Mutable state accumulated during ELK graph construction and extraction. */ /** Mutable state accumulated during ELK graph construction and extraction. */
private static class LayoutContext { private static class LayoutContext {
final ElkGraphFactory factory; final ElkGraphFactory factory;
final Map<String, RouteNode> nodeById;
final Map<String, ElkNode> elkNodeMap = new HashMap<>(); final Map<String, ElkNode> elkNodeMap = new HashMap<>();
final Map<String, Color> nodeColors = new HashMap<>(); final Map<String, Color> nodeColors = new HashMap<>();
final Set<String> compoundNodeIds = new HashSet<>(); final Set<String> compoundNodeIds = new HashSet<>();
@@ -985,8 +1003,9 @@ public class ElkDiagramRenderer implements DiagramRenderer {
final Map<String, List<String>> doTrySectionOrder = new LinkedHashMap<>(); final Map<String, List<String>> doTrySectionOrder = new LinkedHashMap<>();
final Map<String, CompoundInfo> compoundInfos = new HashMap<>(); final Map<String, CompoundInfo> compoundInfos = new HashMap<>();
LayoutContext(ElkGraphFactory factory) { LayoutContext(ElkGraphFactory factory, Map<String, RouteNode> nodeById) {
this.factory = factory; this.factory = factory;
this.nodeById = nodeById;
} }
} }
} }

View File

@@ -65,6 +65,7 @@ const COMPOUND_TYPES = new Set([
'ON_EXCEPTION', 'ERROR_HANDLER', 'ON_EXCEPTION', 'ERROR_HANDLER',
'ON_COMPLETION', 'ON_COMPLETION',
'EIP_CIRCUIT_BREAKER', '_CB_MAIN', '_CB_FALLBACK', 'EIP_CIRCUIT_BREAKER', '_CB_MAIN', '_CB_FALLBACK',
'EIP_FILTER', 'EIP_IDEMPOTENT_CONSUMER', 'EIP_RECIPIENT_LIST',
]); ]);
const ERROR_COMPOUND_TYPES = new Set([ const ERROR_COMPOUND_TYPES = new Set([