From 4af71aabac149cca9daeb837c9e6dac30ddeefe9 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Fri, 27 Mar 2026 22:39:04 +0100 Subject: [PATCH] fix: use graph root + edge walk to separate main flow from handlers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: graph.getNodes() is a flat list with duplicates — handler compound children appear both nested inside their parent AND as top-level entries. The previous separation tried to filter the flat list but missed the duplicates, leaving handler children in rootNode. New approach: walk from graph.getRoot() following non-ERROR edges to discover main flow nodes. Edges targeting handler compounds (ON_EXCEPTION, ON_COMPLETION) are not followed. This cleanly separates main flow from handler sections using the graph's own structure. Falls back to flat list filtering (old behavior) when graph.getRoot() is null (legacy/test graphs). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/diagram/ElkDiagramRenderer.java | 57 +++++++++++++++---- 1 file changed, 46 insertions(+), 11 deletions(-) diff --git a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java index 1d58e754..930088ce 100644 --- a/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java +++ b/cameleer3-server-app/src/main/java/com/cameleer3/server/app/diagram/ElkDiagramRenderer.java @@ -206,24 +206,59 @@ public class ElkDiagramRenderer implements DiagramRenderer { Map nodeColors = new HashMap<>(); Set compoundNodeIds = new HashSet<>(); - // Separate handler sections from main flow nodes. - // graph.getNodes() is a FLAT list that includes handler compound children - // as top-level entries. We must identify and exclude them. + // Build a lookup from the flat nodes list (graph.getNodes() contains + // duplicates — children appear both nested AND top-level). Use this for + // resolving edge endpoints to RouteNode objects. + Map nodeById = new HashMap<>(); + if (graph.getNodes() != null) { + for (RouteNode rn : graph.getNodes()) { + nodeById.put(rn.getId(), rn); + } + } + + // Separate main flow from handler sections using graph structure: + // - Start from graph.getRoot() and walk FLOW edges for main flow + // - Handler sections are top-level nodes of HANDLER_SECTION_TYPES + // This avoids the problem of the flat nodes list containing duplicates. + Set mainNodeIds = new HashSet<>(); List mainNodes = new ArrayList<>(); List handlerNodes = new ArrayList<>(); - Set handlerDescendantIds = new HashSet<>(); + + // Collect main flow node IDs by walking from root along non-ERROR edges. + // graph.getRoot() provides the entry point; ERROR edges lead to handler sections. + if (graph.getRoot() != null && graph.getEdges() != null) { + mainNodeIds.add(graph.getRoot().getId()); + boolean changed = true; + while (changed) { + changed = false; + for (RouteEdge re : graph.getEdges()) { + if (re.getEdgeType() == RouteEdge.EdgeType.ERROR) continue; + if (mainNodeIds.contains(re.getSource()) && !mainNodeIds.contains(re.getTarget())) { + // Don't follow edges INTO handler compounds + RouteNode target = nodeById.get(re.getTarget()); + if (target != null && target.getType() != null + && HANDLER_SECTION_TYPES.contains(target.getType())) { + continue; + } + mainNodeIds.add(re.getTarget()); + changed = true; + } + } + } + } + + // Build node lists — deduplicate (flat list has duplicates) + Set seen = new HashSet<>(); if (graph.getNodes() != null) { - // First pass: identify handler compounds and collect their descendant IDs 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); - collectDescendantIds(rn.getChildren(), handlerDescendantIds); - } - } - // Second pass: main flow = everything that isn't a handler or handler descendant - for (RouteNode rn : graph.getNodes()) { - if (!handlerNodes.contains(rn) && !handlerDescendantIds.contains(rn.getId())) { + } else if (mainNodeIds.isEmpty() || mainNodeIds.contains(rn.getId())) { + // When no root is available (legacy/test graphs), include all + // non-handler nodes as main flow (fallback to old behavior) mainNodes.add(rn); } }