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.EIP_LOOP, NodeType.EIP_MULTICAST,
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. */
@@ -185,15 +186,16 @@ public class ElkDiagramRenderer implements DiagramRenderer {
// ----------------------------------------------------------------
private LayoutResult computeLayout(RouteGraph graph, Direction rootDirection) {
LayoutContext ctx = new LayoutContext(ElkGraphFactory.eINSTANCE);
// 1. Partition graph nodes into main flow vs handler sections
// 1. Build node index from root tree (preserves children) with flat-list fallback
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> handlerNodes = new ArrayList<>();
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);
for (RouteNode rn : mainNodes) {
if (!ctx.elkNodeMap.containsKey(rn.getId())) {
@@ -208,34 +210,53 @@ public class ElkDiagramRenderer implements DiagramRenderer {
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);
// 4. Run layout engine
// 5. Run layout engine
RecursiveGraphLayoutEngine engine = new RecursiveGraphLayoutEngine();
engine.layout(rootNode, new BasicProgressMonitor());
for (ElkNode hr : handlerRoots) {
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);
// 6. Extract positioned result
// 7. Extract positioned result
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) {
Map<String, RouteNode> nodeById = new HashMap<>();
if (graph.getRoot() != null) {
collectNodesRecursive(graph.getRoot(), nodeById);
}
if (graph.getNodes() != null) {
for (RouteNode rn : graph.getNodes()) {
nodeById.put(rn.getId(), rn);
nodeById.putIfAbsent(rn.getId(), rn);
}
}
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
* from the graph root. Handler sections (onException, onCompletion, etc.)
@@ -266,16 +287,14 @@ public class ElkDiagramRenderer implements DiagramRenderer {
}
Set<String> seen = new HashSet<>();
if (graph.getNodes() != null) {
for (RouteNode rn : graph.getNodes()) {
if (seen.contains(rn.getId())) continue;
seen.add(rn.getId());
if (rn.getType() != null && HANDLER_SECTION_TYPES.contains(rn.getType())
&& rn.getChildren() != null && !rn.getChildren().isEmpty()) {
handlerNodes.add(rn);
} else if (mainNodeIds.isEmpty() || mainNodeIds.contains(rn.getId())) {
mainNodes.add(rn);
}
for (RouteNode rn : nodeById.values()) {
if (seen.contains(rn.getId())) continue;
seen.add(rn.getId());
if (rn.getType() != null && HANDLER_SECTION_TYPES.contains(rn.getType())
&& rn.getChildren() != null && !rn.getChildren().isEmpty()) {
handlerNodes.add(rn);
} else if (mainNodeIds.isEmpty() || mainNodeIds.contains(rn.getId())) {
mainNodes.add(rn);
}
}
}
@@ -378,15 +397,13 @@ public class ElkDiagramRenderer implements DiagramRenderer {
/** Extract positioned nodes, edges, and bounding box from the ELK layout result. */
private LayoutResult extractLayout(RouteGraph graph, ElkNode rootNode,
List<ElkNode> handlerRoots, LayoutContext ctx) {
// Extract positioned nodes
// Extract positioned nodes (uses tree-aware nodeById for compound children)
List<PositionedNode> positionedNodes = new ArrayList<>();
if (graph.getNodes() != null) {
for (RouteNode rn : graph.getNodes()) {
if (ctx.childNodeIds.contains(rn.getId())) continue;
ElkNode elkNode = ctx.elkNodeMap.get(rn.getId());
if (elkNode == null) continue;
positionedNodes.add(extractPositionedNode(rn, elkNode, getElkRoot(elkNode), ctx));
}
for (RouteNode rn : ctx.nodeById.values()) {
if (ctx.childNodeIds.contains(rn.getId())) continue;
ElkNode elkNode = ctx.elkNodeMap.get(rn.getId());
if (elkNode == null) continue;
positionedNodes.add(extractPositionedNode(rn, elkNode, getElkRoot(elkNode), ctx));
}
// 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. */
private static class LayoutContext {
final ElkGraphFactory factory;
final Map<String, RouteNode> nodeById;
final Map<String, ElkNode> elkNodeMap = new HashMap<>();
final Map<String, Color> nodeColors = new HashMap<>();
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, CompoundInfo> compoundInfos = new HashMap<>();
LayoutContext(ElkGraphFactory factory) {
LayoutContext(ElkGraphFactory factory, Map<String, RouteNode> nodeById) {
this.factory = factory;
this.nodeById = nodeById;
}
}
}

View File

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