fix: use graph root + edge walk to separate main flow from handlers
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 58s
CI / docker (push) Has been cancelled
CI / deploy (push) Has been cancelled
CI / deploy-feature (push) Has been cancelled

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) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-27 22:39:04 +01:00
parent acb7cade90
commit 4af71aabac

View File

@@ -206,24 +206,59 @@ public class ElkDiagramRenderer implements DiagramRenderer {
Map<String, Color> nodeColors = new HashMap<>(); Map<String, Color> nodeColors = new HashMap<>();
Set<String> compoundNodeIds = new HashSet<>(); Set<String> compoundNodeIds = new HashSet<>();
// Separate handler sections from main flow nodes. // Build a lookup from the flat nodes list (graph.getNodes() contains
// graph.getNodes() is a FLAT list that includes handler compound children // duplicates — children appear both nested AND top-level). Use this for
// as top-level entries. We must identify and exclude them. // resolving edge endpoints to RouteNode objects.
Map<String, RouteNode> 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<String> mainNodeIds = new HashSet<>();
List<RouteNode> mainNodes = new ArrayList<>(); List<RouteNode> mainNodes = new ArrayList<>();
List<RouteNode> handlerNodes = new ArrayList<>(); List<RouteNode> handlerNodes = new ArrayList<>();
Set<String> 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<String> seen = new HashSet<>();
if (graph.getNodes() != null) { if (graph.getNodes() != null) {
// First pass: identify handler compounds and collect their descendant IDs
for (RouteNode rn : graph.getNodes()) { 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()) 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);
collectDescendantIds(rn.getChildren(), handlerDescendantIds); } 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)
// 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())) {
mainNodes.add(rn); mainNodes.add(rn);
} }
} }